create a copy of updates when added

This commit is contained in:
Emmanuel Hansen 2023-08-13 19:22:00 +00:00
parent 7a2fadb499
commit 091d9d4546
2 changed files with 160 additions and 21 deletions

View File

@ -1,16 +1,30 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorageHelper 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.anggrayudi.storage.file.getAbsolutePath
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import java.io.File import java.io.File
import java.util.LinkedList
import java.util.Queue
import kotlin.math.max import kotlin.math.max
class TitleUpdateViewModel(val titleId: String) { class TitleUpdateViewModel(val titleId: String) {
private var basePath: String
private var updateJsonName = "updates.json"
private var stagingUpdateJsonName = "staging_updates.json"
private var storageHelper: SimpleStorageHelper private var storageHelper: SimpleStorageHelper
var pathsState: SnapshotStateList<String>? = null var pathsState: SnapshotStateList<String>? = null
@ -56,19 +70,123 @@ class TitleUpdateViewModel(val titleId: String) {
storageHelper.openFilePicker(UpdateRequestCode) storageHelper.openFilePicker(UpdateRequestCode)
} }
fun save(index: Int) { fun save(
index: Int,
isCopying: MutableState<Boolean>,
openDialog: MutableState<Boolean>,
copyProgress: MutableState<Float>,
currentProgressName: MutableState<String>
) {
data?.apply { data?.apply {
this.selected = "" this.selected = ""
if(paths.isNotEmpty() && index > 0) if (paths.isNotEmpty() && index > 0) {
{
val ind = max(index - 1, paths.count() - 1) val ind = max(index - 1, paths.count() - 1)
this.selected = paths[ind] this.selected = paths[ind]
} }
val gson = Gson() val gson = Gson()
val json = gson.toJson(this) var json = gson.toJson(this)
jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) File(basePath).mkdirs()
File(jsonPath).mkdirs() File("$basePath/$stagingUpdateJsonName").writeText(json)
File("$jsonPath/updates.json").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<String> = 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<String>()
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 private var jsonPath: String
init { 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() data = TitleUpdateMetadata()
if (File(jsonPath).exists()) { if (File(stagingJson).exists()) {
val gson = Gson() val gson = Gson()
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java)
data?.apply { data?.apply {
val existingPaths = mutableListOf<String>() val existingPaths = mutableListOf<String>()
@ -115,4 +235,4 @@ class TitleUpdateViewModel(val titleId: String) {
data class TitleUpdateMetadata( data class TitleUpdateMetadata(
var selected: String = "", var selected: String = "",
var paths: MutableList<String> = mutableListOf() var paths: MutableList<String> = mutableListOf()
) )

View File

@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -35,11 +36,16 @@ class TitleUpdateViews {
val selected = remember { mutableStateOf(0) } val selected = remember { mutableStateOf(0) }
viewModel.data?.apply { viewModel.data?.apply {
selected.value = paths.indexOf(this.selected) + 1 selected.value = paths.indexOf(this.selected) + 1
} }
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
val isCopying = remember {
mutableStateOf(false)
}
val copyProgress = remember {
mutableStateOf(0.0f)
}
Column { Column {
Text(text = "Updates for ${name}", textAlign = TextAlign.Center) Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
Surface( Surface(
@ -50,17 +56,19 @@ class TitleUpdateViews {
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.height(300.dp) .height(250.dp)
.fillMaxWidth() .fillMaxWidth()
) { ) {
Row(modifier = Modifier.padding(8.dp)) { Row(modifier = Modifier.padding(8.dp)) {
RadioButton( RadioButton(
selected = (selected.value == 0), selected = (selected.value == 0),
onClick = { selected.value = 0 onClick = {
}) selected.value = 0
})
Text( Text(
text = "None", text = "None",
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
) )
} }
@ -79,7 +87,8 @@ class TitleUpdateViews {
onClick = { selected.value = i }) onClick = { selected.value = i })
Text( Text(
text = path, text = path,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically) .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)) Spacer(modifier = Modifier.height(18.dp))
TextButton( TextButton(
modifier = Modifier.align(Alignment.End), modifier = Modifier.align(Alignment.End),
onClick = { onClick = {
openDialog.value = false viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName)
viewModel.save(selected.value)
}, },
) { ) {
Text("Save") Text("Save")
@ -126,4 +145,4 @@ class TitleUpdateViews {
} }
} }
} }
} }