forked from MeloNX/MeloNX
android - move title updates support to SAF
This commit is contained in:
parent
df99842106
commit
a718af9dd1
@ -262,7 +262,7 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type)
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type, JInt updateDescriptor)
|
||||
{
|
||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||
if (SwitchDevice?.EmulationContext == null)
|
||||
@ -271,8 +271,9 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
var stream = OpenFile(descriptor);
|
||||
var update = updateDescriptor == -1 ? null : OpenFile(updateDescriptor);
|
||||
|
||||
return LoadApplication(stream, (FileType)(int)type);
|
||||
return LoadApplication(stream, (FileType)(int)type, update);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceVerifyFirmware")]
|
||||
|
@ -77,14 +77,14 @@ namespace LibRyujinx
|
||||
return SwitchDevice?.ContentManager?.VerifyFirmwarePackage(stream, isXci) ?? null;
|
||||
}
|
||||
|
||||
public static bool LoadApplication(Stream stream, FileType type)
|
||||
public static bool LoadApplication(Stream stream, FileType type, Stream? updateStream = null)
|
||||
{
|
||||
var emulationContext = SwitchDevice.EmulationContext;
|
||||
return type switch
|
||||
{
|
||||
FileType.None => false,
|
||||
FileType.Nsp => emulationContext?.LoadNsp(stream) ?? false,
|
||||
FileType.Xci => emulationContext?.LoadXci(stream) ?? false,
|
||||
FileType.Nsp => emulationContext?.LoadNsp(stream, updateStream) ?? false,
|
||||
FileType.Xci => emulationContext?.LoadXci(stream, updateStream) ?? false,
|
||||
FileType.Nro => emulationContext?.LoadProgram(stream, true, "") ?? false,
|
||||
};
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, Stream stream, out string errorMessage, string extension)
|
||||
internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, Stream stream, out string errorMessage, string extension, Stream updateStream = null)
|
||||
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
|
||||
where TFormat : IPartitionFileSystemFormat
|
||||
where THeader : unmanaged, IPartitionFileSystemHeader
|
||||
@ -87,7 +87,13 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
{
|
||||
// Clear the program index part.
|
||||
titleIdBase &= ~0xFUL;
|
||||
|
||||
PartitionFileSystem updatePartitionFileSystem = new();
|
||||
if (updateStream != null)
|
||||
{
|
||||
LoadUpdate(device, updateStream, ref updatePatchNca, ref updateControlNca, titleIdBase, updatePartitionFileSystem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load update information if exists.
|
||||
string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
|
||||
if (File.Exists(titleUpdateMetadataPath))
|
||||
@ -95,34 +101,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
||||
if (File.Exists(updatePath))
|
||||
{
|
||||
PartitionFileSystem updatePartitionFileSystem = new();
|
||||
updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
|
||||
|
||||
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
|
||||
|
||||
// TODO: This should use CNMT NCA instead.
|
||||
foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||
|
||||
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (nca.IsProgram())
|
||||
{
|
||||
updatePatchNca = nca;
|
||||
}
|
||||
else if (nca.IsControl())
|
||||
{
|
||||
updateControlNca = nca;
|
||||
}
|
||||
LoadUpdate(device, new FileStream(updatePath, FileMode.Open, FileAccess.Read), ref updatePatchNca, ref updateControlNca, titleIdBase, updatePartitionFileSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,6 +150,38 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||
errorMessage = "Unable to load: Could not find Main NCA";
|
||||
|
||||
return (false, ProcessResult.Failed);
|
||||
|
||||
static void LoadUpdate(Switch device, Stream updateStream, ref Nca updatePatchNca, ref Nca updateControlNca, ulong titleIdBase, PartitionFileSystem updatePartitionFileSystem)
|
||||
{
|
||||
updatePartitionFileSystem.Initialize(updateStream.AsStorage()).ThrowIfFailure();
|
||||
|
||||
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
|
||||
|
||||
// TODO: This should use CNMT NCA instead.
|
||||
foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
|
||||
|
||||
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (nca.IsProgram())
|
||||
{
|
||||
updatePatchNca = nca;
|
||||
}
|
||||
else if (nca.IsControl())
|
||||
{
|
||||
updateControlNca = nca;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
|
||||
|
@ -39,7 +39,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
return LoadXci(stream);
|
||||
}
|
||||
|
||||
public bool LoadXci(Stream stream)
|
||||
public bool LoadXci(Stream stream, Stream updateStream = null)
|
||||
{
|
||||
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
||||
|
||||
@ -50,7 +50,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
return false;
|
||||
}
|
||||
|
||||
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, stream, out string errorMessage, "xci");
|
||||
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, stream, out string errorMessage, "xci", updateStream);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@ -79,12 +79,12 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
return LoadNsp(file);
|
||||
}
|
||||
|
||||
public bool LoadNsp(Stream stream)
|
||||
public bool LoadNsp(Stream stream, Stream updateStream = null)
|
||||
{
|
||||
PartitionFileSystem partitionFileSystem = new();
|
||||
partitionFileSystem.Initialize(stream.AsStorage()).ThrowIfFailure();
|
||||
|
||||
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, stream, out string errorMessage, "nsp");
|
||||
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, stream, out string errorMessage, "nsp", updateStream);
|
||||
|
||||
if (processResult.ProcessId == 0)
|
||||
{
|
||||
|
@ -93,7 +93,7 @@ namespace Ryujinx.HLE
|
||||
return Processes.LoadNxo(fileName);
|
||||
}
|
||||
|
||||
public bool LoadXci(Stream xciStream)
|
||||
public bool LoadXci(Stream xciStream, Stream updateStream = null)
|
||||
{
|
||||
return Processes.LoadXci(xciStream);
|
||||
}
|
||||
@ -103,9 +103,9 @@ namespace Ryujinx.HLE
|
||||
return Processes.LoadNca(ncaStream);
|
||||
}
|
||||
|
||||
public bool LoadNsp(Stream nspStream)
|
||||
public bool LoadNsp(Stream nspStream, Stream updateStream = null)
|
||||
{
|
||||
return Processes.LoadNsp(nspStream);
|
||||
return Processes.LoadNsp(nspStream, updateStream);
|
||||
}
|
||||
|
||||
public bool LoadProgram(Stream stream, bool isNro, string name)
|
||||
|
@ -39,7 +39,7 @@ class RyujinxNative {
|
||||
external fun deviceGetGameFifo(): Double
|
||||
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
|
||||
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean
|
||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||
external fun graphicsRendererRunLoop()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.ryujinx.android.viewmodels
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.extension
|
||||
@ -9,6 +10,7 @@ import org.ryujinx.android.RyujinxNative
|
||||
|
||||
|
||||
class GameModel(var file: DocumentFile, val context: Context) {
|
||||
private var updateDescriptor: ParcelFileDescriptor? = null
|
||||
var type: FileType
|
||||
var descriptor: ParcelFileDescriptor? = null
|
||||
var fileName: String?
|
||||
@ -50,9 +52,29 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
return descriptor?.fd ?: 0
|
||||
}
|
||||
|
||||
fun openUpdate() : Int {
|
||||
if(titleId?.isNotEmpty() == true) {
|
||||
val vm = TitleUpdateViewModel(titleId ?: "")
|
||||
|
||||
if(vm.data?.selected?.isNotEmpty() == true){
|
||||
val uri = Uri.parse(vm.data?.selected)
|
||||
val file = DocumentFile.fromSingleUri(context, uri)
|
||||
if(file?.exists() == true){
|
||||
updateDescriptor = context.contentResolver.openFileDescriptor(file.uri, "rw")
|
||||
|
||||
return updateDescriptor ?.fd ?: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
fun close() {
|
||||
descriptor?.close()
|
||||
descriptor = null
|
||||
updateDescriptor?.close()
|
||||
updateDescriptor = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,8 @@ class MainViewModel(val activity: MainActivity) {
|
||||
if (descriptor == 0)
|
||||
return false
|
||||
|
||||
val update = game.openUpdate()
|
||||
|
||||
gameModel = game
|
||||
isMiiEditorLaunched = false
|
||||
|
||||
@ -178,7 +180,7 @@ class MainViewModel(val activity: MainActivity) {
|
||||
if (!success)
|
||||
return false
|
||||
|
||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal)
|
||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
|
||||
|
||||
if (!success)
|
||||
return false
|
||||
|
@ -1,12 +1,15 @@
|
||||
package org.ryujinx.android.viewmodels
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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.file.extension
|
||||
import com.google.gson.Gson
|
||||
import org.ryujinx.android.Helpers
|
||||
import org.ryujinx.android.MainActivity
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
@ -16,29 +19,32 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
private var basePath: String
|
||||
private var updateJsonName = "updates.json"
|
||||
private var storageHelper: SimpleStorageHelper
|
||||
var currentPaths: MutableList<String> = mutableListOf()
|
||||
var pathsState: SnapshotStateList<String>? = null
|
||||
|
||||
companion object {
|
||||
const val UpdateRequestCode = 1002
|
||||
}
|
||||
|
||||
fun Remove(index: Int) {
|
||||
fun remove(index: Int) {
|
||||
if (index <= 0)
|
||||
return
|
||||
|
||||
data?.paths?.apply {
|
||||
val removed = removeAt(index - 1)
|
||||
File(removed).deleteRecursively()
|
||||
val str = removeAt(index - 1)
|
||||
Uri.parse(str)?.apply {
|
||||
storageHelper.storage.context.contentResolver.releasePersistableUriPermission(
|
||||
this,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
pathsState?.clear()
|
||||
pathsState?.addAll(this)
|
||||
currentPaths = this
|
||||
}
|
||||
}
|
||||
|
||||
fun Add(
|
||||
isCopying: MutableState<Boolean>,
|
||||
copyProgress: MutableState<Float>,
|
||||
currentProgressName: MutableState<String>
|
||||
) {
|
||||
fun add() {
|
||||
val callBack = storageHelper.onFileSelected
|
||||
|
||||
storageHelper.onFileSelected = { requestCode, files ->
|
||||
@ -47,29 +53,29 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
if (requestCode == UpdateRequestCode) {
|
||||
val file = files.firstOrNull()
|
||||
file?.apply {
|
||||
// Copy updates to internal data folder
|
||||
val updatePath = "$basePath/update"
|
||||
File(updatePath).mkdirs()
|
||||
Helpers.copyToData(
|
||||
this,
|
||||
updatePath,
|
||||
storageHelper,
|
||||
isCopying,
|
||||
copyProgress,
|
||||
currentProgressName, ::refreshPaths
|
||||
)
|
||||
if(file.extension == "nsp"){
|
||||
storageHelper.storage.context.contentResolver.takePersistableUriPermission(file.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
currentPaths.add(file.uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
refreshPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
storageHelper.openFilePicker(UpdateRequestCode)
|
||||
}
|
||||
|
||||
fun refreshPaths() {
|
||||
private fun refreshPaths() {
|
||||
data?.apply {
|
||||
val updatePath = "$basePath/update"
|
||||
val existingPaths = mutableListOf<String>()
|
||||
File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) }
|
||||
currentPaths.forEach {
|
||||
val uri = Uri.parse(it)
|
||||
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||
if(file?.exists() == true){
|
||||
existingPaths.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingPaths.contains(selected)) {
|
||||
selected = ""
|
||||
@ -88,7 +94,6 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
openDialog: MutableState<Boolean>
|
||||
) {
|
||||
data?.apply {
|
||||
val updatePath = "$basePath/update"
|
||||
this.selected = ""
|
||||
if (paths.isNotEmpty() && index > 0) {
|
||||
val ind = max(index - 1, paths.count() - 1)
|
||||
@ -98,18 +103,29 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
File(basePath).mkdirs()
|
||||
|
||||
|
||||
var metadata = TitleUpdateMetadata()
|
||||
val metadata = TitleUpdateMetadata()
|
||||
val savedUpdates = mutableListOf<String>()
|
||||
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
|
||||
currentPaths.forEach {
|
||||
val uri = Uri.parse(it)
|
||||
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||
if(file?.exists() == true){
|
||||
savedUpdates.add(it)
|
||||
}
|
||||
}
|
||||
metadata.paths = savedUpdates
|
||||
|
||||
val selectedName = File(selected).name
|
||||
val newSelectedPath = "$updatePath/$selectedName"
|
||||
if (File(newSelectedPath).exists()) {
|
||||
metadata.selected = newSelectedPath
|
||||
if(selected.isNotEmpty()){
|
||||
val uri = Uri.parse(selected)
|
||||
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||
if(file?.exists() == true){
|
||||
metadata.selected = selected
|
||||
}
|
||||
}
|
||||
else {
|
||||
metadata.selected = selected
|
||||
}
|
||||
|
||||
var json = gson.toJson(metadata)
|
||||
val json = gson.toJson(metadata)
|
||||
File("$basePath/$updateJsonName").writeText(json)
|
||||
|
||||
openDialog.value = false
|
||||
@ -138,9 +154,12 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
||||
|
||||
}
|
||||
currentPaths = data?.paths ?: mutableListOf()
|
||||
storageHelper = MainActivity.StorageHelper!!
|
||||
refreshPaths()
|
||||
|
||||
storageHelper = MainActivity.StorageHelper!!
|
||||
File("$basePath/update").deleteRecursively()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.ryujinx.android.views
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@ -13,7 +14,6 @@ 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
|
||||
@ -28,13 +28,19 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.ryujinx.android.MainActivity
|
||||
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
||||
import java.io.File
|
||||
|
||||
class TitleUpdateViews {
|
||||
companion object {
|
||||
@Composable
|
||||
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
|
||||
fun Main(
|
||||
titleId: String,
|
||||
name: String,
|
||||
openDialog: MutableState<Boolean>,
|
||||
canClose: MutableState<Boolean>
|
||||
) {
|
||||
val viewModel = TitleUpdateViewModel(titleId)
|
||||
|
||||
val selected = remember { mutableStateOf(0) }
|
||||
@ -43,15 +49,6 @@ class TitleUpdateViews {
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
val isCopying = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val copyProgress = remember {
|
||||
mutableStateOf(0.0f)
|
||||
}
|
||||
var currentProgressName = remember {
|
||||
mutableStateOf("Starting Copy")
|
||||
}
|
||||
Column {
|
||||
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
||||
Surface(
|
||||
@ -88,18 +85,24 @@ class TitleUpdateViews {
|
||||
var index = 1
|
||||
for (path in paths) {
|
||||
val i = index
|
||||
val uri = Uri.parse(path)
|
||||
val file = DocumentFile.fromSingleUri(
|
||||
MainActivity.mainViewModel!!.activity,
|
||||
uri
|
||||
)
|
||||
file?.apply {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
RadioButton(
|
||||
selected = (selected.value == i),
|
||||
onClick = { selected.value = i })
|
||||
Text(
|
||||
text = File(path).name,
|
||||
text = file.name ?: "",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
@ -107,7 +110,7 @@ class TitleUpdateViews {
|
||||
Row(modifier = Modifier.align(Alignment.End)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.Remove(selected.value)
|
||||
viewModel.remove(selected.value)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@ -118,7 +121,7 @@ class TitleUpdateViews {
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.Add(isCopying, copyProgress, currentProgressName)
|
||||
viewModel.add()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@ -129,33 +132,12 @@ class TitleUpdateViews {
|
||||
}
|
||||
|
||||
}
|
||||
if (isCopying.value) {
|
||||
Text(text = "Copying updates to local storage")
|
||||
Text(text = currentProgressName.value)
|
||||
Row {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
progress = copyProgress.value
|
||||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
isCopying.value = false
|
||||
canClose.value = true
|
||||
viewModel.refreshPaths()
|
||||
},
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
TextButton(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
onClick = {
|
||||
if (!isCopying.value) {
|
||||
canClose.value = true
|
||||
viewModel.save(selected.value, openDialog)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("Save")
|
||||
|
Loading…
x
Reference in New Issue
Block a user