From ae29ec8ba8bb81fc9007c25fff33bcf2222eec84 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen <emmausssss@gmail.com>
Date: Sun, 30 Jul 2023 18:20:38 +0000
Subject: [PATCH] add dlc manager

---
 src/LibRyujinx/Android/JniExportedMethods.cs  |  37 ++++
 src/LibRyujinx/LibRyujinx.cs                  |  78 ++++++++
 .../main/java/org/ryujinx/android/Icons.kt    | 182 ++++++++++++++++++
 .../java/org/ryujinx/android/RyujinxNative.kt |   2 +
 .../android/viewmodels/DlcViewModel.kt        | 152 +++++++++++++++
 .../org/ryujinx/android/views/DlcViews.kt     | 134 +++++++++++++
 .../org/ryujinx/android/views/HomeViews.kt    |  64 +++++-
 .../org/ryujinx/android/views/MainView.kt     | 104 +---------
 .../ryujinx/android/views/TitleUpdateViews.kt |   2 +-
 9 files changed, 642 insertions(+), 113 deletions(-)
 create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt
 create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt
 create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt

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<string> 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<NewObjectArrayDelegate>();
+            var findClass = findClassPtr.GetUnsafeDelegate<FindClassDelegate>();
+            var setObjectArrayElement = setObjectArrayElementPtr.GetUnsafeDelegate<SetObjectArrayElementDelegate>();
+            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<IFile> 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<string> GetDlcContentList(string path, ulong titleId)
+        {
+            if(!File.Exists(path))
+                return new List<string>();
+
+            using FileStream containerFile = File.OpenRead(path);
+
+            PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+            bool containsDownloadableContent = false;
+
+            SwitchDevice.VirtualFileSystem.ImportTickets(partitionFileSystem);
+            List<string> paths = new List<string>();
+
+            foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
+            {
+                using var ncaFile = new UniqueRef<IFile>();
+
+                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<String>
 }
\ 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<Boolean>) {
+        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<DlcItem>) {
+        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<DlcItem> {
+        var items = mutableListOf<DlcItem>()
+
+        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<DlcContainerList>? = 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<MutableList<DlcContainerList>>() {}.type
+            data =
+                gson.fromJson<MutableList<DlcContainerList>>(File(jsonPath).readText(), typeToken)
+        }
+
+    }
+}
+
+data class DlcContainerList(
+    var path: String = "",
+    var dlc_nca_list: MutableList<DlcContainer> = mutableListOf()
+)
+
+data class DlcContainer(
+    var enabled: Boolean = false,
+    var titleId: String = "",
+    var fullPath: String = "")
+
+data class DlcItem(
+    var name:String = "",
+    var isEnabled: MutableState<Boolean> = 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<Boolean>) {
+            val viewModel = DlcViewModel(titleId)
+
+            var dlcList = remember {
+                mutableListOf<DlcItem>()
+            }
+
+            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"
                             )
                         }
                     }