forked from MeloNX/MeloNX
android - add support for nro
This commit is contained in:
parent
9680ecd820
commit
521403b0ea
@ -237,7 +237,7 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type)
|
||||
{
|
||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||
if (SwitchDevice?.EmulationContext == null)
|
||||
@ -247,7 +247,7 @@ namespace LibRyujinx
|
||||
|
||||
var stream = OpenFile(descriptor);
|
||||
|
||||
return LoadApplication(stream, isXci);
|
||||
return LoadApplication(stream, (FileType)(int)type);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
||||
@ -429,12 +429,12 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci)
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JLong extension)
|
||||
{
|
||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||
using var stream = OpenFile(fileDescriptor);
|
||||
|
||||
var info = GetGameInfo(stream, isXci);
|
||||
var ext = GetStoredString(extension);
|
||||
var info = GetGameInfo(stream, ext.ToLower());
|
||||
return GetInfo(jEnv, info);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using ARMeilleure.Translation;
|
||||
using ARMeilleure.Translation;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Logging;
|
||||
@ -66,10 +66,16 @@ namespace LibRyujinx
|
||||
return LoadApplication(path);
|
||||
}
|
||||
|
||||
public static bool LoadApplication(Stream stream, bool isXci)
|
||||
public static bool LoadApplication(Stream stream, FileType type)
|
||||
{
|
||||
var emulationContext = SwitchDevice.EmulationContext;
|
||||
return (isXci ? emulationContext?.LoadXci(stream) : emulationContext.LoadNsp(stream)) ?? false;
|
||||
return type switch
|
||||
{
|
||||
FileType.None => false,
|
||||
FileType.Nsp => emulationContext?.LoadNsp(stream) ?? false,
|
||||
FileType.Xci => emulationContext?.LoadXci(stream) ?? false,
|
||||
FileType.Nro => emulationContext?.LoadProgram(stream, true, "") ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public static bool LaunchMiiEditApplet()
|
||||
@ -221,5 +227,13 @@ namespace LibRyujinx
|
||||
Renderer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum FileType
|
||||
{
|
||||
None,
|
||||
Nsp,
|
||||
Xci,
|
||||
Nro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,9 @@ using System.Globalization;
|
||||
using Ryujinx.Ui.Common.Configuration.System;
|
||||
using Ryujinx.Common.Logging.Targets;
|
||||
using System.Collections.Generic;
|
||||
using LibHac.Bcat;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System.Text;
|
||||
|
||||
namespace LibRyujinx
|
||||
{
|
||||
@ -126,10 +129,10 @@ namespace LibRyujinx
|
||||
|
||||
using var stream = File.Open(file, FileMode.Open);
|
||||
|
||||
return GetGameInfo(stream, file.ToLower().EndsWith("xci"));
|
||||
return GetGameInfo(stream, new FileInfo(file).Extension.Remove('.'));
|
||||
}
|
||||
|
||||
public static GameInfo? GetGameInfo(Stream gameStream, bool isXci)
|
||||
public static GameInfo? GetGameInfo(Stream gameStream, string extension)
|
||||
{
|
||||
if (SwitchDevice == null)
|
||||
{
|
||||
@ -142,7 +145,7 @@ namespace LibRyujinx
|
||||
FileSize = gameStream.Length * 0.000000000931, TitleName = "Unknown", TitleId = "0000000000000000",
|
||||
Developer = "Unknown",
|
||||
Version = "0",
|
||||
Icon = null,
|
||||
Icon = null
|
||||
};
|
||||
|
||||
const Language TitleLanguage = Language.AmericanEnglish;
|
||||
@ -153,129 +156,169 @@ namespace LibRyujinx
|
||||
{
|
||||
try
|
||||
{
|
||||
IFileSystem pfs;
|
||||
|
||||
bool isExeFs = false;
|
||||
|
||||
if (isXci)
|
||||
if (extension == "nsp" || extension == "pfs0" || extension == "xci")
|
||||
{
|
||||
Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage());
|
||||
IFileSystem pfs;
|
||||
|
||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pfsTemp = new PartitionFileSystem();
|
||||
pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure();
|
||||
pfs = pfsTemp;
|
||||
bool isExeFs = false;
|
||||
|
||||
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
|
||||
bool hasMainNca = false;
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
|
||||
if (extension == "xci")
|
||||
{
|
||||
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
|
||||
Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage());
|
||||
|
||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pfsTemp = new PartitionFileSystem();
|
||||
pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure();
|
||||
pfs = pfsTemp;
|
||||
|
||||
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
|
||||
bool hasMainNca = false;
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new(SwitchDevice.VirtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
|
||||
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
|
||||
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
|
||||
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
|
||||
{
|
||||
hasMainNca = true;
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
break;
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new(SwitchDevice.VirtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
|
||||
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
|
||||
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
|
||||
{
|
||||
hasMainNca = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
|
||||
{
|
||||
isExeFs = true;
|
||||
}
|
||||
}
|
||||
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
|
||||
|
||||
if (!hasMainNca && !isExeFs)
|
||||
{
|
||||
isExeFs = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMainNca && !isExeFs)
|
||||
if (isExeFs)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
using UniqueRef<IFile> npdmFile = new();
|
||||
|
||||
if (isExeFs)
|
||||
{
|
||||
using UniqueRef<IFile> npdmFile = new();
|
||||
Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
|
||||
|
||||
Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
|
||||
|
||||
if (ResultFs.PathNotFound.Includes(result))
|
||||
{
|
||||
Npdm npdm = new(npdmFile.Get.AsStream());
|
||||
|
||||
gameInfo.TitleName = npdm.TitleName;
|
||||
gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id);
|
||||
|
||||
gameInfo.TitleId = id;
|
||||
|
||||
if (controlFs == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there is an update available.
|
||||
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs))
|
||||
{
|
||||
// Replace the original ControlFs by the updated one.
|
||||
controlFs = updatedControlFs;
|
||||
}
|
||||
|
||||
ReadControlData(controlFs, controlHolder.ByteSpan);
|
||||
|
||||
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
||||
|
||||
// Read the icon from the ControlFS and store it as a byte array
|
||||
try
|
||||
{
|
||||
using UniqueRef<IFile> icon = new();
|
||||
|
||||
controlFs?.OpenFile(ref icon.Ref, $"/icon_{TitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
using MemoryStream stream = new();
|
||||
|
||||
icon.Get.AsStream().CopyTo(stream);
|
||||
gameInfo.Icon = stream.ToArray();
|
||||
}
|
||||
catch (HorizonResultException)
|
||||
{
|
||||
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
|
||||
if (ResultFs.PathNotFound.Includes(result))
|
||||
{
|
||||
if (entry.Name == "control.nacp")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Npdm npdm = new(npdmFile.Get.AsStream());
|
||||
|
||||
using var icon = new UniqueRef<IFile>();
|
||||
gameInfo.TitleName = npdm.TitleName;
|
||||
gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id);
|
||||
|
||||
controlFs?.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
gameInfo.TitleId = id;
|
||||
|
||||
if (controlFs == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there is an update available.
|
||||
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs))
|
||||
{
|
||||
// Replace the original ControlFs by the updated one.
|
||||
controlFs = updatedControlFs;
|
||||
}
|
||||
|
||||
ReadControlData(controlFs, controlHolder.ByteSpan);
|
||||
|
||||
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
||||
|
||||
// Read the icon from the ControlFS and store it as a byte array
|
||||
try
|
||||
{
|
||||
using UniqueRef<IFile> icon = new();
|
||||
|
||||
controlFs?.OpenFile(ref icon.Ref, $"/icon_{TitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
using MemoryStream stream = new();
|
||||
|
||||
icon.Get.AsStream().CopyTo(stream);
|
||||
gameInfo.Icon = stream.ToArray();
|
||||
|
||||
if (gameInfo.Icon != null)
|
||||
}
|
||||
catch (HorizonResultException)
|
||||
{
|
||||
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
|
||||
{
|
||||
break;
|
||||
if (entry.Name == "control.nacp")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var icon = new UniqueRef<IFile>();
|
||||
|
||||
controlFs?.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
using MemoryStream stream = new();
|
||||
|
||||
icon.Get.AsStream().CopyTo(stream);
|
||||
gameInfo.Icon = stream.ToArray();
|
||||
|
||||
if (gameInfo.Icon != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (extension == "nro")
|
||||
{
|
||||
BinaryReader reader = new(gameStream);
|
||||
|
||||
byte[] Read(long position, int size)
|
||||
{
|
||||
gameStream.Seek(position, SeekOrigin.Begin);
|
||||
|
||||
return reader.ReadBytes(size);
|
||||
}
|
||||
|
||||
gameStream.Seek(24, SeekOrigin.Begin);
|
||||
|
||||
int assetOffset = reader.ReadInt32();
|
||||
|
||||
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
|
||||
{
|
||||
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
|
||||
|
||||
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
|
||||
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
|
||||
|
||||
ulong nacpOffset = reader.ReadUInt64();
|
||||
ulong nacpSize = reader.ReadUInt64();
|
||||
|
||||
// Reads and stores game icon as byte array
|
||||
if (iconSize > 0)
|
||||
{
|
||||
gameInfo.Icon = Read(assetOffset + iconOffset, (int)iconSize);
|
||||
}
|
||||
|
||||
// Read the NACP data
|
||||
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
|
||||
|
||||
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,9 +33,9 @@ class RyujinxNative {
|
||||
external fun deviceGetGameFrameRate(): Double
|
||||
external fun deviceGetGameFrameTime(): Double
|
||||
external fun deviceGetGameFifo(): Double
|
||||
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo
|
||||
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
|
||||
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
|
||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||
external fun graphicsRendererRunLoop()
|
||||
|
@ -4,10 +4,12 @@ import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import org.ryujinx.android.NativeHelpers
|
||||
import org.ryujinx.android.RyujinxNative
|
||||
|
||||
|
||||
class GameModel(var file: DocumentFile, val context: Context) {
|
||||
var type: FileType
|
||||
var descriptor: ParcelFileDescriptor? = null
|
||||
var fileName: String?
|
||||
var fileSize = 0.0
|
||||
@ -19,8 +21,9 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
|
||||
init {
|
||||
fileName = file.name
|
||||
var pid = open()
|
||||
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci"))
|
||||
val pid = open()
|
||||
val ext = NativeHelpers.instance.storeStringJava(file.extension)
|
||||
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext)
|
||||
close()
|
||||
|
||||
fileSize = gameInfo.FileSize
|
||||
@ -29,6 +32,16 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
developer = gameInfo.Developer
|
||||
version = gameInfo.Version
|
||||
icon = gameInfo.Icon
|
||||
type = when {
|
||||
(file.extension == "xci") -> FileType.Xci
|
||||
(file.extension == "nsp") -> FileType.Nsp
|
||||
(file.extension == "nro") -> FileType.Nro
|
||||
else -> FileType.None
|
||||
}
|
||||
|
||||
if (type == FileType.Nro && (titleName.isNullOrEmpty() || titleName == "Unknown")) {
|
||||
titleName = file.name
|
||||
}
|
||||
}
|
||||
|
||||
fun open() : Int {
|
||||
@ -41,10 +54,6 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
descriptor?.close()
|
||||
descriptor = null
|
||||
}
|
||||
|
||||
fun isXci() : Boolean {
|
||||
return file.extension == "xci"
|
||||
}
|
||||
}
|
||||
|
||||
class GameInfo {
|
||||
@ -55,3 +64,10 @@ class GameInfo {
|
||||
var Version: String? = null
|
||||
var Icon: String? = null
|
||||
}
|
||||
|
||||
enum class FileType{
|
||||
None,
|
||||
Nsp,
|
||||
Xci,
|
||||
Nro
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ class HomeViewModel(
|
||||
loadedCache.clear()
|
||||
val files = mutableListOf<GameModel>()
|
||||
for (file in folder.search(false, DocumentFileType.FILE)) {
|
||||
if (file.extension == "xci" || file.extension == "nsp")
|
||||
if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro")
|
||||
activity.let {
|
||||
val item = GameModel(file, it)
|
||||
|
||||
|
@ -165,7 +165,7 @@ class MainViewModel(val activity: MainActivity) {
|
||||
if (!success)
|
||||
return false
|
||||
|
||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
|
||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal)
|
||||
|
||||
if (!success)
|
||||
return false
|
||||
|
@ -57,11 +57,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||
import org.ryujinx.android.R
|
||||
import org.ryujinx.android.viewmodels.FileType
|
||||
import org.ryujinx.android.viewmodels.GameModel
|
||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
@ -398,7 +401,7 @@ class HomeViews {
|
||||
selected = null
|
||||
}
|
||||
selectedModel.value = null
|
||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
|
||||
thread {
|
||||
showLoading.value = true
|
||||
val success =
|
||||
@ -427,7 +430,7 @@ class HomeViews {
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row {
|
||||
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
||||
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
|
||||
if (gameModel.icon?.isNotEmpty() == true) {
|
||||
val pic = decoder.decode(gameModel.icon)
|
||||
val size =
|
||||
@ -441,7 +444,9 @@ class HomeViews {
|
||||
.width(size.roundToInt().dp)
|
||||
.height(size.roundToInt().dp)
|
||||
)
|
||||
} else NotAvailableIcon()
|
||||
} else if (gameModel.type == FileType.Nro)
|
||||
NROIcon()
|
||||
else NotAvailableIcon()
|
||||
} else NotAvailableIcon()
|
||||
Column {
|
||||
Text(text = gameModel.titleName ?: "")
|
||||
@ -487,7 +492,7 @@ class HomeViews {
|
||||
selected = null
|
||||
}
|
||||
selectedModel.value = null
|
||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
|
||||
thread {
|
||||
showLoading.value = true
|
||||
val success =
|
||||
@ -510,7 +515,7 @@ class HomeViews {
|
||||
})
|
||||
) {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
||||
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
|
||||
if (gameModel.icon?.isNotEmpty() == true) {
|
||||
val pic = decoder.decode(gameModel.icon)
|
||||
val size = GridImageSize / Resources.getSystem().displayMetrics.density
|
||||
@ -523,20 +528,24 @@ class HomeViews {
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
} else NotAvailableIcon()
|
||||
} else if (gameModel.type == FileType.Nro)
|
||||
NROIcon()
|
||||
else NotAvailableIcon()
|
||||
} else NotAvailableIcon()
|
||||
Text(
|
||||
text = gameModel.titleName ?: "N/A",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.basicMarquee()
|
||||
)
|
||||
Text(
|
||||
text = gameModel.developer ?: "N/A",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.basicMarquee()
|
||||
)
|
||||
}
|
||||
@ -556,6 +565,19 @@ class HomeViews {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NROIcon() {
|
||||
val size = ListImageSize / Resources.getSystem().displayMetrics.density
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.icon_nro),
|
||||
contentDescription = "NRO",
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.width(size.roundToInt().dp)
|
||||
.height(size.roundToInt().dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
BIN
src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png
Normal file
BIN
src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Loading…
x
Reference in New Issue
Block a user