diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index 013b9bbec..7e6351421 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -152,6 +152,22 @@ namespace LibRyujinx return LoadApplication(path); } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcContentList")] + public static JArrayLocalRef JniGetDlcContentListNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JLong titleId) + { + var list = GetDlcContentList(GetString(jEnv, pathPtr), (ulong)(long)titleId); + + debug_break(4); + + return CreateStringArray(jEnv, list); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetDlcTitleId")] + public static JStringLocalRef JniGetDlcTitleIdNative(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef pathPtr, JStringLocalRef ncaPath) + { + return CreateString(jEnv, GetDlcTitleId(GetString(jEnv, pathPtr), GetString(jEnv, ncaPath))); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceSignalEmulationClose")] public static void JniSignalEmulationCloseNative(JEnvRef jEnv, JObjectLocalRef jObj) { @@ -296,6 +312,27 @@ namespace LibRyujinx return InitializeGraphicsRenderer(GraphicsBackend.Vulkan, createSurfaceFunc, extensions.ToArray()); } + private static JArrayLocalRef CreateStringArray(JEnvRef jEnv, List strings) + { + JEnvValue value = jEnv.Environment; + ref JNativeInterface jInterface = ref value.Functions; + IntPtr newObjectArrayPtr = jInterface.NewObjectArrayPointer; + IntPtr findClassPtr = jInterface.FindClassPointer; + IntPtr setObjectArrayElementPtr = jInterface.SetObjectArrayElementPointer; + + var newObjectArray = newObjectArrayPtr.GetUnsafeDelegate(); + var findClass = findClassPtr.GetUnsafeDelegate(); + var setObjectArrayElement = setObjectArrayElementPtr.GetUnsafeDelegate(); + var array = newObjectArray(jEnv, strings.Count, findClass(jEnv, GetCCharSequence("java/lang/String")), CreateString(jEnv, "")._value); + + for (int i = 0; i < strings.Count; i++) + { + setObjectArrayElement(jEnv, array, i, CreateString(jEnv, strings[i])._value); + } + + return array; + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsRendererSetSize")] public static void JniSetRendererSizeNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt width, JInt height) { diff --git a/src/LibRyujinx/LibRyujinx.cs b/src/LibRyujinx/LibRyujinx.cs index 2e0657d53..6e832e06a 100644 --- a/src/LibRyujinx/LibRyujinx.cs +++ b/src/LibRyujinx/LibRyujinx.cs @@ -31,6 +31,7 @@ using Ryujinx.Common.Utilities; using System.Globalization; using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Common.Logging.Targets; +using System.Collections.Generic; namespace LibRyujinx { @@ -545,6 +546,83 @@ namespace LibRyujinx return gameInfo; } + + public static string GetDlcTitleId(string path, string ncaPath) + { + if (File.Exists(path)) + { + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + + SwitchDevice.VirtualFileSystem.ImportTickets(partitionFileSystem); + + using UniqueRef ncaFile = new(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref, ncaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), ncaPath); + if (nca != null) + { + return nca.Header.TitleId.ToString("X16"); + } + } + + return string.Empty; + } + + + private static Nca TryOpenNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(SwitchDevice.VirtualFileSystem.KeySet, ncaStorage); + } + catch (Exception ex) + { + } + + return null; + } + + public static List GetDlcContentList(string path, ulong titleId) + { + if(!File.Exists(path)) + return new List(); + + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + bool containsDownloadableContent = false; + + SwitchDevice.VirtualFileSystem.ImportTickets(partitionFileSystem); + List paths = new List(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path); + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != titleId) + { + break; + } + + paths.Add(fileEntry.FullPath); + } + } + + return paths; + } } public class SwitchDevice : IDisposable diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt new file mode 100644 index 000000000..96aa04bba --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -0,0 +1,182 @@ +package org.ryujinx.android + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +class Icons { + companion object{ + /// Icons exported from https://www.composables.com/icons + @Composable + fun Download(): ImageVector { + return remember { + ImageVector.Builder( + name = "download", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(20f, 26.25f) + quadToRelative(-0.25f, 0f, -0.479f, -0.083f) + quadToRelative(-0.229f, -0.084f, -0.438f, -0.292f) + lineToRelative(-6.041f, -6.083f) + quadToRelative(-0.417f, -0.375f, -0.396f, -0.917f) + quadToRelative(0.021f, -0.542f, 0.396f, -0.917f) + reflectiveQuadToRelative(0.916f, -0.396f) + quadToRelative(0.542f, -0.02f, 0.959f, 0.396f) + lineToRelative(3.791f, 3.792f) + verticalLineTo(8.292f) + quadToRelative(0f, -0.584f, 0.375f, -0.959f) + reflectiveQuadTo(20f, 6.958f) + quadToRelative(0.542f, 0f, 0.938f, 0.375f) + quadToRelative(0.395f, 0.375f, 0.395f, 0.959f) + verticalLineTo(21.75f) + lineToRelative(3.792f, -3.792f) + quadToRelative(0.375f, -0.416f, 0.917f, -0.396f) + quadToRelative(0.541f, 0.021f, 0.958f, 0.396f) + quadToRelative(0.375f, 0.375f, 0.375f, 0.917f) + reflectiveQuadToRelative(-0.375f, 0.958f) + lineToRelative(-6.083f, 6.042f) + quadToRelative(-0.209f, 0.208f, -0.438f, 0.292f) + quadToRelative(-0.229f, 0.083f, -0.479f, 0.083f) + close() + moveTo(9.542f, 32.958f) + quadToRelative(-1.042f, 0f, -1.834f, -0.791f) + quadToRelative(-0.791f, -0.792f, -0.791f, -1.834f) + verticalLineToRelative(-4.291f) + quadToRelative(0f, -0.542f, 0.395f, -0.938f) + quadToRelative(0.396f, -0.396f, 0.938f, -0.396f) + quadToRelative(0.542f, 0f, 0.917f, 0.396f) + reflectiveQuadToRelative(0.375f, 0.938f) + verticalLineToRelative(4.291f) + horizontalLineToRelative(20.916f) + verticalLineToRelative(-4.291f) + quadToRelative(0f, -0.542f, 0.375f, -0.938f) + quadToRelative(0.375f, -0.396f, 0.917f, -0.396f) + quadToRelative(0.583f, 0f, 0.958f, 0.396f) + reflectiveQuadToRelative(0.375f, 0.938f) + verticalLineToRelative(4.291f) + quadToRelative(0f, 1.042f, -0.791f, 1.834f) + quadToRelative(-0.792f, 0.791f, -1.834f, 0.791f) + close() + } + }.build() + } + } + @Composable + fun VideoGame(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "videogame_asset", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + fillAlpha = 1f, + stroke = SolidColor(primaryColor), + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 29.792f) + quadToRelative(-1.083f, 0f, -1.854f, -0.792f) + quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f) + verticalLineTo(12.833f) + quadToRelative(0f, -1.083f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(27.5f) + quadToRelative(1.083f, 0f, 1.854f, 0.771f) + quadToRelative(0.771f, 0.771f, 0.771f, 1.854f) + verticalLineToRelative(14.334f) + quadToRelative(0f, 1.041f, -0.771f, 1.833f) + reflectiveQuadToRelative(-1.854f, 0.792f) + close() + moveToRelative(0f, -2.625f) + horizontalLineToRelative(27.5f) + verticalLineTo(12.833f) + horizontalLineTo(6.25f) + verticalLineToRelative(14.334f) + close() + moveToRelative(7.167f, -1.792f) + quadToRelative(0.541f, 0f, 0.916f, -0.375f) + reflectiveQuadToRelative(0.375f, -0.917f) + verticalLineToRelative(-2.791f) + horizontalLineToRelative(2.75f) + quadToRelative(0.584f, 0f, 0.959f, -0.375f) + reflectiveQuadToRelative(0.375f, -0.917f) + quadToRelative(0f, -0.542f, -0.375f, -0.938f) + quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f) + horizontalLineToRelative(-2.75f) + verticalLineToRelative(-2.75f) + quadToRelative(0f, -0.542f, -0.375f, -0.938f) + quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f) + quadToRelative(-0.584f, 0f, -0.959f, 0.396f) + reflectiveQuadToRelative(-0.375f, 0.938f) + verticalLineToRelative(2.75f) + horizontalLineToRelative(-2.75f) + quadToRelative(-0.541f, 0f, -0.937f, 0.395f) + quadTo(8f, 19.458f, 8f, 20f) + quadToRelative(0f, 0.542f, 0.396f, 0.917f) + reflectiveQuadToRelative(0.937f, 0.375f) + horizontalLineToRelative(2.75f) + verticalLineToRelative(2.791f) + quadToRelative(0f, 0.542f, 0.396f, 0.917f) + reflectiveQuadToRelative(0.938f, 0.375f) + close() + moveToRelative(11.125f, -0.5f) + quadToRelative(0.791f, 0f, 1.396f, -0.583f) + quadToRelative(0.604f, -0.584f, 0.604f, -1.375f) + quadToRelative(0f, -0.834f, -0.604f, -1.417f) + quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f) + quadToRelative(-0.834f, 0f, -1.417f, 0.583f) + quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) + quadToRelative(0f, 0.833f, 0.583f, 1.417f) + quadToRelative(0.583f, 0.583f, 1.417f, 0.583f) + close() + moveToRelative(3.916f, -5.833f) + quadToRelative(0.834f, 0f, 1.417f, -0.584f) + quadToRelative(0.583f, -0.583f, 0.583f, -1.416f) + quadToRelative(0f, -0.792f, -0.583f, -1.375f) + quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f) + quadToRelative(-0.791f, 0f, -1.375f, 0.584f) + quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) + quadToRelative(0f, 0.833f, 0.583f, 1.416f) + quadToRelative(0.584f, 0.584f, 1.375f, 0.584f) + close() + moveTo(6.25f, 27.167f) + verticalLineTo(12.833f) + verticalLineToRelative(14.334f) + close() + } + }.build() + } + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index c3e54b5d6..c89259147 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -50,4 +50,6 @@ class RyujinxNative { external fun graphicsSetSurface(surface: Long) external fun deviceCloseEmulation() external fun deviceSignalEmulationClose() + external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String + external fun deviceGetDlcContentList(path: String, titleId: Long) : Array } \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt new file mode 100644 index 000000000..1d440c11c --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt @@ -0,0 +1,152 @@ +package org.ryujinx.android.viewmodels + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.file.getAbsolutePath +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.ryujinx.android.MainActivity +import org.ryujinx.android.RyujinxNative +import java.io.File + +class DlcViewModel(val titleId: String) { + private var storageHelper: SimpleStorageHelper + + companion object { + const val UpdateRequestCode = 1002 + } + + fun remove(item: DlcItem) { + data?.apply { + this.removeAll { it.path == item.containerPath } + } + } + + fun add(refresh: MutableState) { + val callBack = storageHelper.onFileSelected + + storageHelper.onFileSelected = { requestCode, files -> + run { + storageHelper.onFileSelected = callBack + if (requestCode == UpdateRequestCode) { + val file = files.firstOrNull() + file?.apply { + val path = file.getAbsolutePath(storageHelper.storage.context) + if (path.isNotEmpty()) { + data?.apply { + var contents = RyujinxNative().deviceGetDlcContentList( + path, + titleId.toLong(16) + ) + + if (contents.isNotEmpty()) { + val contentPath = path + val container = DlcContainerList(contentPath); + + for (content in contents) + container.dlc_nca_list.add( + DlcContainer( + true, + titleId, + content + ) + ) + + this.add(container) + } + } + } + } + refresh.value = true + } + } + } + storageHelper.openFilePicker(UpdateRequestCode) + } + + fun save(items: List) { + data?.apply { + + val gson = Gson() + val json = gson.toJson(this) + jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + File(jsonPath).mkdirs() + File("$jsonPath/dlc.json").writeText(json) + } + } + + @Composable + fun getDlc(): List { + var items = mutableListOf() + + data?.apply { + for (container in this) { + val containerPath = container.path + + if (!File(containerPath).exists()) + continue; + + for (dlc in container.dlc_nca_list) { + val enabled = remember { + mutableStateOf(dlc.enabled) + } + items.add( + DlcItem( + File(containerPath).name, + enabled, + containerPath, + dlc.fullPath, + RyujinxNative().deviceGetDlcTitleId(containerPath, dlc.fullPath) + ) + ) + } + } + } + + return items.toList() + } + + var data: MutableList? = null + private var jsonPath: String + + init { + jsonPath = + MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json" + storageHelper = MainActivity.StorageHelper!! + + reloadFromDisk() + } + + private fun reloadFromDisk() { + data = mutableListOf() + if (File(jsonPath).exists()) { + val gson = Gson() + val typeToken = object : TypeToken>() {}.type + data = + gson.fromJson>(File(jsonPath).readText(), typeToken) + } + + } +} + +data class DlcContainerList( + var path: String = "", + var dlc_nca_list: MutableList = mutableListOf() +) + +data class DlcContainer( + var enabled: Boolean = false, + var titleId: String = "", + var fullPath: String = "") + +data class DlcItem( + var name:String = "", + var isEnabled: MutableState = mutableStateOf(false), + var containerPath: String = "", + var fullPath: String = "", + var titleId: String = "") \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt new file mode 100644 index 000000000..ea14f654b --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt @@ -0,0 +1,134 @@ +package org.ryujinx.android.views + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.ryujinx.android.viewmodels.DlcItem +import org.ryujinx.android.viewmodels.DlcViewModel + +class DlcViews { + companion object { + @Composable + fun Main(titleId: String, name: String, openDialog: MutableState) { + val viewModel = DlcViewModel(titleId) + + var dlcList = remember { + mutableListOf() + } + + viewModel.data?.apply { + dlcList.clear() + } + + var refresh = remember { + mutableStateOf(true) + } + + Column(modifier = Modifier.padding(16.dp)) { + Column { + Row(modifier = Modifier.padding(8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween) { + Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align( + Alignment.CenterVertically + )) + IconButton( + onClick = { + viewModel.add(refresh) + }, + modifier = Modifier.align( + Alignment.CenterVertically + ) + ) { + Icon( + Icons.Filled.Add, + contentDescription = "Add" + ) + } + } + Surface( + modifier = Modifier + .padding(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium + ) { + + if(refresh.value) { + dlcList.clear() + dlcList.addAll(viewModel.getDlc()) + refresh.value = false + } + LazyColumn(modifier = Modifier + .fillMaxWidth() + .height(400.dp)){ + items(dlcList) { dlcItem -> + dlcItem.apply { + Row(modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + Checkbox( + checked = (dlcItem.isEnabled.value), + onCheckedChange = { dlcItem.isEnabled.value = it }) + Text( + text = dlcItem.name, + modifier = Modifier + .align(Alignment.CenterVertically) + .wrapContentWidth(Alignment.Start) + .fillMaxWidth(0.9f) + ) + IconButton( + onClick = { + viewModel.remove(dlcItem) + refresh.value = true + }) { + Icon(Icons.Filled.Delete, + contentDescription = "remove" + ) + } + } + } + } + } + } + + } + Spacer(modifier = Modifier.height(8.dp)) + TextButton( + modifier = Modifier.align(Alignment.End), + onClick = { + openDialog.value = false + viewModel.save(dlcList) + }, + ) { + Text("Save") + } + } + } + } +} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index d61ac1e6d..4d9b56fea 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -56,7 +56,6 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.zIndex import androidx.navigation.NavHostController @@ -236,11 +235,12 @@ class HomeViews { showBottomSheet.value = false }, sheetState = sheetState) { - val openDialog = remember { mutableStateOf(false) } + val openTitleUpdateDialog = remember { mutableStateOf(false) } + val openDlcDialog = remember { mutableStateOf(false) } - if(openDialog.value) { + if(openTitleUpdateDialog.value) { AlertDialog(onDismissRequest = { - openDialog.value = false + openTitleUpdateDialog.value = false }) { Surface( modifier = Modifier @@ -251,24 +251,43 @@ class HomeViews { ) { val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" val name = viewModel.mainViewModel?.selected?.titleName ?: "" - TitleUpdateViews.Main(titleId, name, openDialog) + TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog) } } } + if(openDlcDialog.value) { + AlertDialog(onDismissRequest = { + openDlcDialog.value = false + }) { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" + val name = viewModel.mainViewModel?.selected?.titleName ?: "" + DlcViews.Main(titleId, name, openDlcDialog) + } + + } + } Surface(color = MaterialTheme.colorScheme.surface, modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { Card( + modifier = Modifier.padding(8.dp), onClick = { - openDialog.value = true + openTitleUpdateDialog.value = true } ) { Column(modifier = Modifier.padding(16.dp)) { Icon( painter = painterResource(R.drawable.app_update), - contentDescription = "More", + contentDescription = "Game Updates", tint = Color.Green, modifier = Modifier .width(48.dp) @@ -281,6 +300,28 @@ class HomeViews { } } + Card( + modifier = Modifier.padding(8.dp), + onClick = { + openDlcDialog.value = true + } + ) { + Column(modifier = Modifier.padding(16.dp)) { + Icon( + imageVector = org.ryujinx.android.Icons.Download(), + contentDescription = "Game Dlc", + tint = Color.Green, + modifier = Modifier + .width(48.dp) + .height(48.dp) + .align(Alignment.CenterHorizontally) + ) + Text(text = "Game DLC", + modifier = Modifier.align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.onSurface) + + } + } } } } @@ -307,10 +348,13 @@ class HomeViews { runBlocking { launch { showLoading.value = true - val success = viewModel.mainViewModel?.loadGame(gameModel) ?: false - if(success) { + val success = + viewModel.mainViewModel?.loadGame(gameModel) ?: false + if (success) { launchOnUiThread { - viewModel.mainViewModel?.activity?.setFullScreen(true) + viewModel.mainViewModel?.activity?.setFullScreen( + true + ) viewModel.mainViewModel?.navController?.navigate("game") } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index edb60e12a..b37699768 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -1,14 +1,12 @@ package org.ryujinx.android.views import androidx.activity.compose.BackHandler -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth @@ -19,7 +17,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -38,12 +35,12 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import org.ryujinx.android.GameController import org.ryujinx.android.GameHost +import org.ryujinx.android.Icons import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.SettingsViewModel @@ -143,7 +140,7 @@ class MainView { mainViewModel.controller?.setVisible(!mainViewModel.controller!!.isVisible) }) { Icon( - imageVector = rememberVideogameAsset(), + imageVector = Icons.VideoGame(), contentDescription = "Toggle Virtual Pad" ) } @@ -201,103 +198,6 @@ class MainView { } } } - - @Composable - fun rememberVideogameAsset(): ImageVector { - val primaryColor = MaterialTheme.colorScheme.primary - return remember { - ImageVector.Builder( - name = "videogame_asset", - defaultWidth = 40.0.dp, - defaultHeight = 40.0.dp, - viewportWidth = 40.0f, - viewportHeight = 40.0f - ).apply { - path( - fill = SolidColor(Color.Black.copy(alpha = 0.5f)), - fillAlpha = 1f, - stroke = SolidColor(primaryColor), - strokeAlpha = 1f, - strokeLineWidth = 1.0f, - strokeLineCap = StrokeCap.Butt, - strokeLineJoin = StrokeJoin.Miter, - strokeLineMiter = 1f, - pathFillType = PathFillType.NonZero - ) { - moveTo(6.25f, 29.792f) - quadToRelative(-1.083f, 0f, -1.854f, -0.792f) - quadToRelative(-0.771f, -0.792f, -0.771f, -1.833f) - verticalLineTo(12.833f) - quadToRelative(0f, -1.083f, 0.771f, -1.854f) - quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) - horizontalLineToRelative(27.5f) - quadToRelative(1.083f, 0f, 1.854f, 0.771f) - quadToRelative(0.771f, 0.771f, 0.771f, 1.854f) - verticalLineToRelative(14.334f) - quadToRelative(0f, 1.041f, -0.771f, 1.833f) - reflectiveQuadToRelative(-1.854f, 0.792f) - close() - moveToRelative(0f, -2.625f) - horizontalLineToRelative(27.5f) - verticalLineTo(12.833f) - horizontalLineTo(6.25f) - verticalLineToRelative(14.334f) - close() - moveToRelative(7.167f, -1.792f) - quadToRelative(0.541f, 0f, 0.916f, -0.375f) - reflectiveQuadToRelative(0.375f, -0.917f) - verticalLineToRelative(-2.791f) - horizontalLineToRelative(2.75f) - quadToRelative(0.584f, 0f, 0.959f, -0.375f) - reflectiveQuadToRelative(0.375f, -0.917f) - quadToRelative(0f, -0.542f, -0.375f, -0.938f) - quadToRelative(-0.375f, -0.395f, -0.959f, -0.395f) - horizontalLineToRelative(-2.75f) - verticalLineToRelative(-2.75f) - quadToRelative(0f, -0.542f, -0.375f, -0.938f) - quadToRelative(-0.375f, -0.396f, -0.916f, -0.396f) - quadToRelative(-0.584f, 0f, -0.959f, 0.396f) - reflectiveQuadToRelative(-0.375f, 0.938f) - verticalLineToRelative(2.75f) - horizontalLineToRelative(-2.75f) - quadToRelative(-0.541f, 0f, -0.937f, 0.395f) - quadTo(8f, 19.458f, 8f, 20f) - quadToRelative(0f, 0.542f, 0.396f, 0.917f) - reflectiveQuadToRelative(0.937f, 0.375f) - horizontalLineToRelative(2.75f) - verticalLineToRelative(2.791f) - quadToRelative(0f, 0.542f, 0.396f, 0.917f) - reflectiveQuadToRelative(0.938f, 0.375f) - close() - moveToRelative(11.125f, -0.5f) - quadToRelative(0.791f, 0f, 1.396f, -0.583f) - quadToRelative(0.604f, -0.584f, 0.604f, -1.375f) - quadToRelative(0f, -0.834f, -0.604f, -1.417f) - quadToRelative(-0.605f, -0.583f, -1.396f, -0.583f) - quadToRelative(-0.834f, 0f, -1.417f, 0.583f) - quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) - quadToRelative(0f, 0.833f, 0.583f, 1.417f) - quadToRelative(0.583f, 0.583f, 1.417f, 0.583f) - close() - moveToRelative(3.916f, -5.833f) - quadToRelative(0.834f, 0f, 1.417f, -0.584f) - quadToRelative(0.583f, -0.583f, 0.583f, -1.416f) - quadToRelative(0f, -0.792f, -0.583f, -1.375f) - quadToRelative(-0.583f, -0.584f, -1.417f, -0.584f) - quadToRelative(-0.791f, 0f, -1.375f, 0.584f) - quadToRelative(-0.583f, 0.583f, -0.583f, 1.375f) - quadToRelative(0f, 0.833f, 0.583f, 1.416f) - quadToRelative(0.584f, 0.584f, 1.375f, 0.584f) - close() - moveTo(6.25f, 27.167f) - verticalLineTo(12.833f) - verticalLineToRelative(14.334f) - close() - } - }.build() - } - } - @Composable fun GameStats(mainViewModel: MainViewModel) { val fifo = remember { 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 ce832199a..ff0900268 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 @@ -107,7 +107,7 @@ class TitleUpdateViews { ) { Icon( Icons.Filled.Add, - contentDescription = "Remove" + contentDescription = "Add" ) } }