forked from MeloNX/MeloNX
create a copy of updates when added
This commit is contained in:
parent
7a2fadb499
commit
091d9d4546
@ -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()
|
||||||
)
|
)
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user