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