forked from MeloNX/MeloNX
android - add grid list option
android - adjust grid view design, remove bottom app bar android - reload list if game folder changed, fix game updates scanning android - set nativeaot instruction set support android - bump version android - bump version android - add log export, providers to browse app data android - add log settings android - add button to open ryujinx app folder android - allow sensor to change orientation during emulation android - add support for nro android - add motion support android - implement firmware installation android - ensure controller respects users controller visibility settings at launch android - fix settings app action buttons. fix dlc manager add button missing android - add hack to fix orientation issue android - fix stick showing as dpad android - set controller event as handled android - add option to swap button layouts to nintendo style android - add basic software keyboard support android - add option to disable motion android - remote developer name from grid items android - fix dpad input on generic android controllers android - move title updates support to SAF android - change game stats background color
This commit is contained in:
parent
eac63c756e
commit
28df6f1c4e
@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Logging.Formatters;
|
using Ryujinx.Common.Logging.Formatters;
|
||||||
using Ryujinx.Common.Logging.Targets;
|
using Ryujinx.Common.Logging.Targets;
|
||||||
using System;
|
using System;
|
||||||
|
136
src/LibRyujinx/Android/AndroidUiHandler.cs
Normal file
136
src/LibRyujinx/Android/AndroidUiHandler.cs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
using LibHac.Tools.Fs;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE;
|
||||||
|
using Ryujinx.HLE.HOS.Applets;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||||
|
using Ryujinx.HLE.Ui;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LibRyujinx.Android
|
||||||
|
{
|
||||||
|
internal class AndroidUiHandler : IHostUiHandler, IDisposable
|
||||||
|
{
|
||||||
|
public IHostUiTheme HostUiTheme => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public ManualResetEvent _waitEvent;
|
||||||
|
public ManualResetEvent _responseEvent;
|
||||||
|
private bool _isDisposed;
|
||||||
|
private bool _isOkPressed;
|
||||||
|
private long _input;
|
||||||
|
|
||||||
|
public AndroidUiHandler()
|
||||||
|
{
|
||||||
|
_waitEvent = new ManualResetEvent(false);
|
||||||
|
_responseEvent = new ManualResetEvent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
|
||||||
|
{
|
||||||
|
LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString(title ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(message ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerType(1);
|
||||||
|
|
||||||
|
_responseEvent.Reset();
|
||||||
|
Set();
|
||||||
|
_responseEvent.WaitOne();
|
||||||
|
|
||||||
|
return _isOkPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
|
||||||
|
{
|
||||||
|
LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString("Software Keyboard"));
|
||||||
|
LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(args.HeaderText ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerWatermark(LibRyujinx.storeString(args.GuideText ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerSubtitle(LibRyujinx.storeString(args.SubtitleText ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerInitialText(LibRyujinx.storeString(args.InitialText ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerMinLength(args.StringLengthMin);
|
||||||
|
LibRyujinx.setUiHandlerMaxLength(args.StringLengthMax);
|
||||||
|
LibRyujinx.setUiHandlerType(2);
|
||||||
|
LibRyujinx.setUiHandlerKeyboardMode((int)args.KeyboardMode);
|
||||||
|
|
||||||
|
_responseEvent.Reset();
|
||||||
|
Set();
|
||||||
|
_responseEvent.WaitOne();
|
||||||
|
|
||||||
|
userText = _input != -1 ? LibRyujinx.GetStoredString(_input) : "";
|
||||||
|
|
||||||
|
return _isOkPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayMessageDialog(string title, string message)
|
||||||
|
{
|
||||||
|
LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString(title ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(message ?? ""));
|
||||||
|
LibRyujinx.setUiHandlerType(1);
|
||||||
|
|
||||||
|
_responseEvent.Reset();
|
||||||
|
Set();
|
||||||
|
_responseEvent.WaitOne();
|
||||||
|
|
||||||
|
return _isOkPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayMessageDialog(ControllerAppletUiArgs args)
|
||||||
|
{
|
||||||
|
string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
|
||||||
|
|
||||||
|
string message = $"Application requests **{playerCount}** player(s) with:\n\n"
|
||||||
|
+ $"**TYPES:** {args.SupportedStyles}\n\n"
|
||||||
|
+ $"**PLAYERS:** {string.Join(", ", args.SupportedPlayers)}\n\n"
|
||||||
|
+ (args.IsDocked ? "Docked mode set. `Handheld` is also invalid.\n\n" : "")
|
||||||
|
+ "_Please reconfigure Input now and then press OK._";
|
||||||
|
|
||||||
|
return DisplayMessageDialog("Controller Applet", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
|
||||||
|
{
|
||||||
|
// throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Wait()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
_waitEvent.Reset();
|
||||||
|
_waitEvent.WaitOne();
|
||||||
|
_waitEvent.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Set()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
_waitEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetResponse(bool isOkPressed, long input)
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
_isOkPressed = isOkPressed;
|
||||||
|
_input = input;
|
||||||
|
_responseEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_isDisposed = true;
|
||||||
|
_waitEvent.Set();
|
||||||
|
_waitEvent.Set();
|
||||||
|
_responseEvent.Dispose();
|
||||||
|
_waitEvent.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
using LibRyujinx.Jni;
|
using LibRyujinx.Jni;
|
||||||
using LibRyujinx.Jni.Pointers;
|
using LibRyujinx.Jni.Pointers;
|
||||||
using LibRyujinx.Jni.Primitives;
|
using LibRyujinx.Jni.Primitives;
|
||||||
using LibRyujinx.Jni.References;
|
using LibRyujinx.Jni.References;
|
||||||
@ -42,11 +42,36 @@ namespace LibRyujinx
|
|||||||
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
||||||
|
|
||||||
[DllImport("libryujinxjni")]
|
[DllImport("libryujinxjni")]
|
||||||
private extern static long storeString(string ch);
|
internal extern static long storeString(string ch);
|
||||||
[DllImport("libryujinxjni")]
|
|
||||||
private extern static IntPtr getString(long id);
|
|
||||||
|
|
||||||
private static string GetStoredString(long id)
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static IntPtr getString(long id);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerTitle(long title);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerMessage(long message);
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerWatermark(long watermark);
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerInitialText(long text);
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerSubtitle(long text);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerType(int type);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerKeyboardMode(int mode);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerMinLength(int lenght);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
internal extern static long setUiHandlerMaxLength(int lenght);
|
||||||
|
|
||||||
|
internal static string GetStoredString(long id)
|
||||||
{
|
{
|
||||||
var pointer = getString(id);
|
var pointer = getString(id);
|
||||||
if (pointer != IntPtr.Zero)
|
if (pointer != IntPtr.Zero)
|
||||||
@ -84,21 +109,21 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")]
|
||||||
public static JBoolean JniInitialize(JEnvRef jEnv, JObjectLocalRef jObj, JLong jpathId, JBoolean enableDebugLogs)
|
public static JBoolean JniInitialize(JEnvRef jEnv, JObjectLocalRef jObj, JLong jpathId)
|
||||||
{
|
{
|
||||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
PlatformInfo.IsBionic = true;
|
PlatformInfo.IsBionic = true;
|
||||||
|
|
||||||
Logger.AddTarget(
|
Logger.AddTarget(
|
||||||
new AsyncLogTargetWrapper(
|
new AsyncLogTargetWrapper(
|
||||||
new AndroidLogTarget("Ryujinx"),
|
new AndroidLogTarget("RyujinxLog"),
|
||||||
1000,
|
1000,
|
||||||
AsyncLogTargetOverflowAction.Block
|
AsyncLogTargetOverflowAction.Block
|
||||||
));
|
));
|
||||||
|
|
||||||
var path = GetStoredString(jpathId);
|
var path = GetStoredString(jpathId);
|
||||||
|
|
||||||
var init = Initialize(path, enableDebugLogs);
|
var init = Initialize(path);
|
||||||
|
|
||||||
_surfaceEvent?.Set();
|
_surfaceEvent?.Set();
|
||||||
|
|
||||||
@ -237,7 +262,7 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
||||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type, JInt updateDescriptor)
|
||||||
{
|
{
|
||||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
if (SwitchDevice?.EmulationContext == null)
|
if (SwitchDevice?.EmulationContext == null)
|
||||||
@ -246,8 +271,62 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stream = OpenFile(descriptor);
|
var stream = OpenFile(descriptor);
|
||||||
|
var update = updateDescriptor == -1 ? null : OpenFile(updateDescriptor);
|
||||||
|
|
||||||
return LoadApplication(stream, isXci);
|
return LoadApplication(stream, (FileType)(int)type, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceVerifyFirmware")]
|
||||||
|
public static JLong JniVerifyFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
|
|
||||||
|
var stream = OpenFile(descriptor);
|
||||||
|
|
||||||
|
long stringHandle = -1;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var version = VerifyFirmware(stream, isXci);
|
||||||
|
|
||||||
|
if (version != null)
|
||||||
|
{
|
||||||
|
stringHandle = storeString(version.VersionString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception _)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInstallFirmware")]
|
||||||
|
public static void JniInstallFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
|
|
||||||
|
var stream = OpenFile(descriptor);
|
||||||
|
|
||||||
|
InstallFirmware(stream, isXci);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetInstalledFirmwareVersion")]
|
||||||
|
public static JLong JniGetInstalledFirmwareVersion(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
|
|
||||||
|
var version = SwitchDevice?.ContentManager.GetCurrentFirmwareVersion();
|
||||||
|
|
||||||
|
long stringHandle = -1;
|
||||||
|
|
||||||
|
if (version != null)
|
||||||
|
{
|
||||||
|
stringHandle = storeString(version.VersionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
||||||
@ -413,6 +492,13 @@ namespace LibRyujinx
|
|||||||
RunLoop();
|
RunLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_loggingSetEnabled")]
|
||||||
|
public static void JniSetLoggingEnabledNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt logLevel, JBoolean enabled)
|
||||||
|
{
|
||||||
|
Logger.SetEnable((LogLevel)(int)logLevel, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")]
|
||||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path)
|
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path)
|
||||||
{
|
{
|
||||||
@ -422,12 +508,12 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
[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");
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
using var stream = OpenFile(fileDescriptor);
|
using var stream = OpenFile(fileDescriptor);
|
||||||
|
var ext = GetStoredString(extension);
|
||||||
var info = GetGameInfo(stream, isXci);
|
var info = GetGameInfo(stream, ext.ToLower());
|
||||||
return GetInfo(jEnv, info);
|
return GetInfo(jEnv, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,6 +632,20 @@ namespace LibRyujinx
|
|||||||
SetButtonReleased((GamepadButtonInputId)(int)button, id);
|
SetButtonReleased((GamepadButtonInputId)(int)button, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetAccelerometerData")]
|
||||||
|
public static void JniSetAccelerometerData(JEnvRef jEnv, JObjectLocalRef jObj, JFloat x, JFloat y, JFloat z, JInt id)
|
||||||
|
{
|
||||||
|
var accel = new Vector3(x, y, z);
|
||||||
|
SetAccelerometerData(accel, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetGyroData")]
|
||||||
|
public static void JniSetGyroData(JEnvRef jEnv, JObjectLocalRef jObj, JFloat x, JFloat y, JFloat z, JInt id)
|
||||||
|
{
|
||||||
|
var gryo = new Vector3(x, y, z);
|
||||||
|
SetGryoData(gryo, id);
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")]
|
||||||
public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id)
|
public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id)
|
||||||
{
|
{
|
||||||
@ -635,6 +735,30 @@ namespace LibRyujinx
|
|||||||
DeleteUser(userId);
|
DeleteUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerSetup")]
|
||||||
|
public static void JniSetupUiHandler(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||||
|
{
|
||||||
|
SetupUiHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerWait")]
|
||||||
|
public static void JniWaitUiHandler(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||||
|
{
|
||||||
|
WaitUiHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerStopWait")]
|
||||||
|
public static void JniStopUiHandlerWait(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||||
|
{
|
||||||
|
StopUiHandlerWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerSetResponse")]
|
||||||
|
public static void JniSetUiHandlerResponse(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean isOkPressed, JLong input)
|
||||||
|
{
|
||||||
|
SetUiHandlerResponse(isOkPressed, input);
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")]
|
||||||
public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr)
|
public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr)
|
||||||
{
|
{
|
||||||
|
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "org.ryujinx.android"
|
applicationId "org.ryujinx.android"
|
||||||
minSdk 30
|
minSdk 30
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 10004
|
versionCode 10010
|
||||||
versionName '1.0.4'
|
versionName '1.0.10'
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -49,6 +49,7 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
prefab true
|
prefab true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion '1.3.2'
|
kotlinCompilerExtensionVersion '1.3.2'
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".RyujinxApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:appCategory="game"
|
android:appCategory="game"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@ -39,6 +40,26 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="org.ryujinx.android.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name=".providers.DocumentProvider"
|
||||||
|
android:authorities="org.ryujinx.android.providers"
|
||||||
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
|
</intent-filter>
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -38,6 +38,36 @@
|
|||||||
|
|
||||||
void* _ryujinxNative = NULL;
|
void* _ryujinxNative = NULL;
|
||||||
|
|
||||||
|
class UiHandler {
|
||||||
|
public:
|
||||||
|
void setTitle(long storedTitle);
|
||||||
|
void setMessage(long storedMessage);
|
||||||
|
void setWatermark(long wm);
|
||||||
|
void setType(int t);
|
||||||
|
void setMode(int t);
|
||||||
|
void setMinLength(int t);
|
||||||
|
void setMaxLength(int t);
|
||||||
|
void setInitialText(long text);
|
||||||
|
void setSubtitle(long text);
|
||||||
|
|
||||||
|
long getTitle();
|
||||||
|
long getMessage();
|
||||||
|
long getWatermark();
|
||||||
|
long getInitialText();
|
||||||
|
long getSubtitle();
|
||||||
|
int type = 0;
|
||||||
|
int keyboardMode = 0;
|
||||||
|
int min_length = -1;
|
||||||
|
int max_length = -1;
|
||||||
|
|
||||||
|
private:
|
||||||
|
long title = -1;
|
||||||
|
long message = -1;
|
||||||
|
long watermark = -1;
|
||||||
|
long initialText = -1;
|
||||||
|
long subtitle = -1;
|
||||||
|
};
|
||||||
|
|
||||||
// Ryujinx imported functions
|
// Ryujinx imported functions
|
||||||
bool (*initialize)(char*) = NULL;
|
bool (*initialize)(char*) = NULL;
|
||||||
|
|
||||||
@ -46,7 +76,7 @@ long _currentRenderingThreadId = 0;
|
|||||||
JavaVM* _vm = nullptr;
|
JavaVM* _vm = nullptr;
|
||||||
jobject _mainActivity = nullptr;
|
jobject _mainActivity = nullptr;
|
||||||
jclass _mainActivityClass = nullptr;
|
jclass _mainActivityClass = nullptr;
|
||||||
std::string _currentString = "";
|
|
||||||
string_helper str_helper = string_helper();
|
string_helper str_helper = string_helper();
|
||||||
|
UiHandler ui_handler = UiHandler();
|
||||||
|
|
||||||
#endif //RYUJINXNATIVE_RYUIJNX_H
|
#endif //RYUJINXNATIVE_RYUIJNX_H
|
||||||
|
@ -185,6 +185,8 @@ void setProgressInfo(char* info, float progressValue) {
|
|||||||
progress = progressValue;
|
progress = progressValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isInitialOrientationFlipped = true;
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
void setCurrentTransform(long native_window, int transform){
|
void setCurrentTransform(long native_window, int transform){
|
||||||
if(native_window == 0 || native_window == -1)
|
if(native_window == 0 || native_window == -1)
|
||||||
@ -204,7 +206,7 @@ void setCurrentTransform(long native_window, int transform){
|
|||||||
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90;
|
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90;
|
||||||
break;
|
break;
|
||||||
case 0x4:
|
case 0x4:
|
||||||
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180;
|
nativeTransform = isInitialOrientationFlipped ? ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY : ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180;
|
||||||
break;
|
break;
|
||||||
case 0x8:
|
case 0x8:
|
||||||
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270;
|
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270;
|
||||||
@ -325,6 +327,37 @@ const char* getString(long id){
|
|||||||
return cstr;
|
return cstr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
void setUiHandlerTitle(long title) {
|
||||||
|
ui_handler.setTitle(title);
|
||||||
|
}
|
||||||
|
void setUiHandlerMessage(long message) {
|
||||||
|
ui_handler.setMessage(message);
|
||||||
|
}
|
||||||
|
void setUiHandlerWatermark(long wm) {
|
||||||
|
ui_handler.setWatermark(wm);
|
||||||
|
}
|
||||||
|
void setUiHandlerType(int type) {
|
||||||
|
ui_handler.setType(type);
|
||||||
|
}
|
||||||
|
void setUiHandlerKeyboardMode(int mode) {
|
||||||
|
ui_handler.setMode(mode);
|
||||||
|
}
|
||||||
|
void setUiHandlerMinLength(int length) {
|
||||||
|
ui_handler.setMinLength(length);
|
||||||
|
}
|
||||||
|
void setUiHandlerMaxLength(int length) {
|
||||||
|
ui_handler.setMaxLength(length);
|
||||||
|
}
|
||||||
|
void setUiHandlerInitialText(long text) {
|
||||||
|
ui_handler.setInitialText(text);
|
||||||
|
}
|
||||||
|
void setUiHandlerSubtitle(long text) {
|
||||||
|
ui_handler.setSubtitle(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jlong JNICALL
|
JNIEXPORT jlong JNICALL
|
||||||
Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz, jstring string) {
|
Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz, jstring string) {
|
||||||
@ -335,5 +368,154 @@ Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz
|
|||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) {
|
Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) {
|
||||||
return createStringFromStdString(env, str_helper.get_stored(id));
|
return createStringFromStdString(env, id > -1 ? str_helper.get_stored(id) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_setIsInitialOrientationFlipped(JNIEnv *env, jobject thiz,
|
||||||
|
jboolean is_flipped) {
|
||||||
|
isInitialOrientationFlipped = is_flipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestType(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.type;
|
||||||
|
}
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestTitle(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.getTitle();
|
||||||
|
}
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestMessage(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void UiHandler::setTitle(long storedTitle) {
|
||||||
|
if(title != -1){
|
||||||
|
str_helper.get_stored(title);
|
||||||
|
title = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
title = storedTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setMessage(long storedMessage) {
|
||||||
|
if(message != -1){
|
||||||
|
str_helper.get_stored(message);
|
||||||
|
message = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = storedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setType(int t) {
|
||||||
|
this->type = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
long UiHandler::getTitle() {
|
||||||
|
auto v = title;
|
||||||
|
title = -1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
long UiHandler::getMessage() {
|
||||||
|
auto v = message;
|
||||||
|
message = -1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setWatermark(long wm) {
|
||||||
|
if(watermark != -1){
|
||||||
|
str_helper.get_stored(watermark);
|
||||||
|
watermark = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
watermark = wm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setMinLength(int t) {
|
||||||
|
this->min_length = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setMaxLength(int t) {
|
||||||
|
this->max_length = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
long UiHandler::getWatermark() {
|
||||||
|
auto v = watermark;
|
||||||
|
watermark = -1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setInitialText(long text) {
|
||||||
|
if(initialText != -1){
|
||||||
|
str_helper.get_stored(watermark);
|
||||||
|
initialText = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setSubtitle(long text) {
|
||||||
|
if(subtitle != -1){
|
||||||
|
str_helper.get_stored(subtitle);
|
||||||
|
subtitle = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitle = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
long UiHandler::getInitialText() {
|
||||||
|
auto v = initialText;
|
||||||
|
initialText = -1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
long UiHandler::getSubtitle() {
|
||||||
|
auto v = subtitle;
|
||||||
|
subtitle = -1;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UiHandler::setMode(int t) {
|
||||||
|
keyboardMode = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerMinLength(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.min_length;
|
||||||
|
}
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerMaxLength(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.max_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestWatermark(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.getWatermark();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestInitialText(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.getInitialText();
|
||||||
|
}
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestSubtitle(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.getSubtitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_org_ryujinx_android_NativeHelpers_getUiHandlerKeyboardMode(JNIEnv *env, jobject thiz) {
|
||||||
|
return ui_handler.keyboardMode;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
package org.ryujinx.android
|
package org.ryujinx.android
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.PersistableBundle
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
|
||||||
abstract class BaseActivity : ComponentActivity() {
|
abstract class BaseActivity : ComponentActivity() {
|
||||||
companion object{
|
companion object{
|
||||||
val crashHandler = CrashHandler()
|
val crashHandler = CrashHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
|
||||||
super.onCreate(savedInstanceState, persistentState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ class CrashHandler : UncaughtExceptionHandler {
|
|||||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
override fun uncaughtException(t: Thread, e: Throwable) {
|
||||||
crashLog += e.toString() + "\n"
|
crashLog += e.toString() + "\n"
|
||||||
|
|
||||||
File(MainActivity.AppPath + "${File.separator}crash.log").writeText(crashLog)
|
File(MainActivity.AppPath + "${File.separator}Logs${File.separator}crash.log").writeText(crashLog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.swordfish.radialgamepad.library.RadialGamePad
|
import com.swordfish.radialgamepad.library.RadialGamePad
|
||||||
import com.swordfish.radialgamepad.library.config.ButtonConfig
|
import com.swordfish.radialgamepad.library.config.ButtonConfig
|
||||||
@ -29,6 +27,7 @@ import kotlinx.coroutines.flow.merge
|
|||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
|
import org.ryujinx.android.viewmodels.QuickSettings
|
||||||
|
|
||||||
typealias GamePad = RadialGamePad
|
typealias GamePad = RadialGamePad
|
||||||
typealias GamePadConfig = RadialGamePadConfig
|
typealias GamePadConfig = RadialGamePadConfig
|
||||||
@ -65,6 +64,7 @@ class GameController(var activity: Activity) {
|
|||||||
}
|
}
|
||||||
controller.controllerView = c
|
controller.controllerView = c
|
||||||
viewModel.setGameController(controller)
|
viewModel.setGameController(controller)
|
||||||
|
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
|
||||||
c
|
c
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import android.view.SurfaceView
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import org.ryujinx.android.viewmodels.GameModel
|
import org.ryujinx.android.viewmodels.GameModel
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
import org.ryujinx.android.viewmodels.QuickSettings
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@ -76,6 +75,8 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||||||
_isInit = false
|
_isInit = false
|
||||||
_isStarted = false
|
_isStarted = false
|
||||||
|
|
||||||
|
mainViewModel.activity.uiHandler.stop()
|
||||||
|
|
||||||
_updateThread?.join()
|
_updateThread?.join()
|
||||||
_renderingThreadWatcher?.join()
|
_renderingThreadWatcher?.join()
|
||||||
}
|
}
|
||||||
@ -88,21 +89,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||||||
|
|
||||||
_nativeRyujinx.inputInitialize(width, height)
|
_nativeRyujinx.inputInitialize(width, height)
|
||||||
|
|
||||||
val settings = QuickSettings(mainViewModel.activity)
|
val id = mainViewModel.physicalControllerManager?.connect()
|
||||||
|
mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
|
||||||
if (!settings.useVirtualController) {
|
|
||||||
mainViewModel.controller?.setVisible(false)
|
|
||||||
} else {
|
|
||||||
mainViewModel.controller?.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
mainViewModel.physicalControllerManager?.connect()
|
|
||||||
|
|
||||||
_nativeRyujinx.graphicsRendererSetSize(
|
_nativeRyujinx.graphicsRendererSetSize(
|
||||||
surfaceHolder.surfaceFrame.width(),
|
surfaceHolder.surfaceFrame.width(),
|
||||||
surfaceHolder.surfaceFrame.height()
|
surfaceHolder.surfaceFrame.height()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
|
||||||
|
|
||||||
_guestThread = thread(start = true) {
|
_guestThread = thread(start = true) {
|
||||||
runGame()
|
runGame()
|
||||||
}
|
}
|
||||||
@ -167,6 +163,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||||||
mainViewModel.performanceManager?.closeCurrentRenderingSession()
|
mainViewModel.performanceManager?.closeCurrentRenderingSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread {
|
||||||
|
mainViewModel.activity.uiHandler.listen()
|
||||||
|
}
|
||||||
_nativeRyujinx.graphicsRendererRunLoop()
|
_nativeRyujinx.graphicsRendererRunLoop()
|
||||||
|
|
||||||
game?.close()
|
game?.close()
|
||||||
|
@ -72,6 +72,7 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyToData(
|
fun copyToData(
|
||||||
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
|
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
|
||||||
isCopying: MutableState<Boolean>,
|
isCopying: MutableState<Boolean>,
|
||||||
@ -79,17 +80,18 @@ class Helpers {
|
|||||||
currentProgressName: MutableState<String>,
|
currentProgressName: MutableState<String>,
|
||||||
finish: () -> Unit
|
finish: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
var fPath = path + "/${file.name}";
|
||||||
var callback: FileCallback? = object : FileCallback() {
|
var callback: FileCallback? = object : FileCallback() {
|
||||||
override fun onFailed(errorCode: FileCallback.ErrorCode) {
|
override fun onFailed(errorCode: FileCallback.ErrorCode) {
|
||||||
super.onFailed(errorCode)
|
super.onFailed(errorCode)
|
||||||
File(path).delete()
|
File(fPath).delete()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(file: Any, workerThread: Thread): Long {
|
override fun onStart(file: Any, workerThread: Thread): Long {
|
||||||
copyProgress.value = 0f
|
copyProgress.value = 0f
|
||||||
|
|
||||||
(file as DocumentFile)?.apply {
|
(file as DocumentFile).apply {
|
||||||
currentProgressName.value = "Copying ${file.name}"
|
currentProgressName.value = "Copying ${file.name}"
|
||||||
}
|
}
|
||||||
return super.onStart(file, workerThread)
|
return super.onStart(file, workerThread)
|
||||||
@ -113,8 +115,8 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
val ioScope = CoroutineScope(Dispatchers.IO)
|
val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
isCopying.value = true
|
isCopying.value = true
|
||||||
|
File(fPath).delete()
|
||||||
file.apply {
|
file.apply {
|
||||||
if (!File(path + "/${file.name}").exists()) {
|
|
||||||
val f = this
|
val f = this
|
||||||
ioScope.launch {
|
ioScope.launch {
|
||||||
f.copyFileTo(
|
f.copyFileTo(
|
||||||
@ -126,7 +128,6 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDataColumn(
|
private fun getDataColumn(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
package org.ryujinx.android
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import net.lingala.zip4j.ZipFile
|
||||||
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
class Logging(private var viewModel: MainViewModel) {
|
||||||
|
val logPath = MainActivity.AppPath + "/Logs"
|
||||||
|
init{
|
||||||
|
File(logPath).mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestExport(){
|
||||||
|
val files = File(logPath).listFiles()
|
||||||
|
files?.apply {
|
||||||
|
val zipExportPath = MainActivity.AppPath + "/log.zip"
|
||||||
|
File(zipExportPath).delete()
|
||||||
|
var count = 0
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
val zipFile = ZipFile(zipExportPath)
|
||||||
|
for (file in files) {
|
||||||
|
if(file.isFile) {
|
||||||
|
zipFile.addFile(file)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zipFile.close()
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
val zip =File (zipExportPath)
|
||||||
|
val uri = FileProvider.getUriForFile(viewModel.activity, viewModel.activity.packageName + ".fileprovider", zip)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
intent.setDataAndType(uri, URLConnection.guessContentTypeFromName(zip.name))
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
val chooser = Intent.createChooser(intent, "Share logs");
|
||||||
|
viewModel.activity.startActivity(chooser)
|
||||||
|
} else {
|
||||||
|
File(zipExportPath).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLogs() {
|
||||||
|
if(File(logPath).exists()){
|
||||||
|
File(logPath).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
File(logPath).mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class LogLevel {
|
||||||
|
Debug, Stub, Info, Warning, Error, Guest, AccessLog, Notice, Trace
|
||||||
|
}
|
@ -18,8 +18,10 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
|
import com.halilibo.richtext.ui.RichTextThemeIntegration
|
||||||
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
|
import org.ryujinx.android.viewmodels.QuickSettings
|
||||||
import org.ryujinx.android.views.MainView
|
import org.ryujinx.android.views.MainView
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@ -27,9 +29,11 @@ import kotlin.math.abs
|
|||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
private var physicalControllerManager: PhysicalControllerManager =
|
private var physicalControllerManager: PhysicalControllerManager =
|
||||||
PhysicalControllerManager(this)
|
PhysicalControllerManager(this)
|
||||||
|
private lateinit var motionSensorManager: MotionSensorManager
|
||||||
private var _isInit: Boolean = false
|
private var _isInit: Boolean = false
|
||||||
var isGameRunning = false
|
var isGameRunning = false
|
||||||
var storageHelper: SimpleStorageHelper? = null
|
var storageHelper: SimpleStorageHelper? = null
|
||||||
|
lateinit var uiHandler: UiHandler
|
||||||
companion object {
|
companion object {
|
||||||
var mainViewModel: MainViewModel? = null
|
var mainViewModel: MainViewModel? = null
|
||||||
var AppPath : String = ""
|
var AppPath : String = ""
|
||||||
@ -64,12 +68,27 @@ class MainActivity : BaseActivity() {
|
|||||||
return
|
return
|
||||||
|
|
||||||
val appPath: String = AppPath
|
val appPath: String = AppPath
|
||||||
val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath), false)
|
|
||||||
|
var quickSettings = QuickSettings(this)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, quickSettings.enableDebugLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, quickSettings.enableInfoLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Stub.ordinal, quickSettings.enableStubLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Warning.ordinal, quickSettings.enableWarningLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Error.ordinal, quickSettings.enableErrorLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.AccessLog.ordinal, quickSettings.enableAccessLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Guest.ordinal, quickSettings.enableGuestLogs)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, quickSettings.enableTraceLogs)
|
||||||
|
val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath))
|
||||||
|
|
||||||
|
uiHandler = UiHandler()
|
||||||
_isInit = success
|
_isInit = success
|
||||||
}
|
}
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
motionSensorManager = MotionSensorManager(this)
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
||||||
|
|
||||||
if(
|
if(
|
||||||
!Environment.isExternalStorageManager()
|
!Environment.isExternalStorageManager()
|
||||||
) {
|
) {
|
||||||
@ -78,7 +97,6 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
||||||
|
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
@ -86,10 +104,14 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
mainViewModel = MainViewModel(this)
|
mainViewModel = MainViewModel(this)
|
||||||
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||||
|
mainViewModel!!.motionSensorManager = motionSensorManager
|
||||||
|
|
||||||
|
mainViewModel!!.refreshFirmwareVersion()
|
||||||
|
|
||||||
mainViewModel?.apply {
|
mainViewModel?.apply {
|
||||||
setContent {
|
setContent {
|
||||||
RyujinxAndroidTheme {
|
RyujinxAndroidTheme {
|
||||||
|
RichTextThemeIntegration(contentColor = { MaterialTheme.colorScheme.onSurface }) {
|
||||||
// A surface container using the 'background' color from the theme
|
// A surface container using the 'background' color from the theme
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -101,6 +123,7 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
storageHelper?.onSaveInstanceState(outState)
|
storageHelper?.onSaveInstanceState(outState)
|
||||||
@ -133,7 +156,7 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
fun setFullScreen(fullscreen: Boolean) {
|
fun setFullScreen(fullscreen: Boolean) {
|
||||||
requestedOrientation =
|
requestedOrientation =
|
||||||
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||||
|
|
||||||
val insets = WindowCompat.getInsetsController(window, window.decorView)
|
val insets = WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
|
||||||
@ -183,6 +206,8 @@ class MainActivity : BaseActivity() {
|
|||||||
setFullScreen(true)
|
setFullScreen(true)
|
||||||
NativeHelpers.instance.setTurboMode(true)
|
NativeHelpers.instance.setTurboMode(true)
|
||||||
force60HzRefreshRate(true)
|
force60HzRefreshRate(true)
|
||||||
|
if (QuickSettings(this).enableMotion)
|
||||||
|
motionSensorManager.register()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,5 +218,7 @@ class MainActivity : BaseActivity() {
|
|||||||
NativeHelpers.instance.setTurboMode(false)
|
NativeHelpers.instance.setTurboMode(false)
|
||||||
force60HzRefreshRate(false)
|
force60HzRefreshRate(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
motionSensorManager.unregister()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
package org.ryujinx.android
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.hardware.Sensor
|
||||||
|
import android.hardware.SensorEvent
|
||||||
|
import android.hardware.SensorEventListener2
|
||||||
|
import android.hardware.SensorManager
|
||||||
|
import android.view.OrientationEventListener
|
||||||
|
|
||||||
|
class MotionSensorManager(val activity: MainActivity) : SensorEventListener2 {
|
||||||
|
private var isRegistered: Boolean = false
|
||||||
|
private var gyro: Sensor?
|
||||||
|
private var accelerometer: Sensor?
|
||||||
|
private var sensorManager: SensorManager =
|
||||||
|
activity.getSystemService(Activity.SENSOR_SERVICE) as SensorManager
|
||||||
|
private var controllerId: Int = -1
|
||||||
|
|
||||||
|
private val motionGyroOrientation : FloatArray = FloatArray(3)
|
||||||
|
private val motionAcelOrientation : FloatArray = FloatArray(3)
|
||||||
|
init {
|
||||||
|
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||||
|
gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||||
|
setOrientation90()
|
||||||
|
var orientationListener = object : OrientationEventListener(activity){
|
||||||
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
|
when{
|
||||||
|
isWithinOrientationRange(orientation, 270) -> {
|
||||||
|
setOrientation270()
|
||||||
|
}
|
||||||
|
isWithinOrientationRange(orientation, 90) -> {
|
||||||
|
setOrientation90()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWithinOrientationRange(
|
||||||
|
currentOrientation : Int, targetOrientation : Int, epsilon : Int = 90
|
||||||
|
) : Boolean {
|
||||||
|
return currentOrientation > targetOrientation - epsilon
|
||||||
|
&& currentOrientation < targetOrientation + epsilon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOrientation270() {
|
||||||
|
motionGyroOrientation[0] = -1.0f
|
||||||
|
motionGyroOrientation[1] = 1.0f
|
||||||
|
motionGyroOrientation[2] = 1.0f
|
||||||
|
motionAcelOrientation[0] = 1.0f
|
||||||
|
motionAcelOrientation[1] = -1.0f
|
||||||
|
motionAcelOrientation[2] = -1.0f
|
||||||
|
}
|
||||||
|
fun setOrientation90() {
|
||||||
|
motionGyroOrientation[0] = 1.0f
|
||||||
|
motionGyroOrientation[1] = -1.0f
|
||||||
|
motionGyroOrientation[2] = 1.0f
|
||||||
|
motionAcelOrientation[0] = -1.0f
|
||||||
|
motionAcelOrientation[1] = 1.0f
|
||||||
|
motionAcelOrientation[2] = -1.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControllerId(id: Int){
|
||||||
|
controllerId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(){
|
||||||
|
if(isRegistered)
|
||||||
|
return
|
||||||
|
gyro?.apply {
|
||||||
|
sensorManager.registerListener(this@MotionSensorManager, gyro, SensorManager.SENSOR_DELAY_GAME)
|
||||||
|
}
|
||||||
|
accelerometer?.apply {
|
||||||
|
sensorManager.registerListener(this@MotionSensorManager, accelerometer, SensorManager.SENSOR_DELAY_GAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister(){
|
||||||
|
sensorManager.unregisterListener(this)
|
||||||
|
isRegistered = false
|
||||||
|
|
||||||
|
if (controllerId != -1){
|
||||||
|
RyujinxNative.instance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId)
|
||||||
|
RyujinxNative.instance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSensorChanged(event: SensorEvent?) {
|
||||||
|
if (controllerId != -1)
|
||||||
|
if (isRegistered)
|
||||||
|
event?.apply {
|
||||||
|
when (sensor.type) {
|
||||||
|
Sensor.TYPE_ACCELEROMETER -> {
|
||||||
|
val x = motionAcelOrientation[0] * event.values[1]
|
||||||
|
val y = motionAcelOrientation[1] * event.values[0]
|
||||||
|
val z = motionAcelOrientation[2] * event.values[2]
|
||||||
|
|
||||||
|
RyujinxNative.instance.inputSetAccelerometerData(x, y, z, controllerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Sensor.TYPE_GYROSCOPE -> {
|
||||||
|
val x = motionGyroOrientation[0] * event.values[1]
|
||||||
|
val y = motionGyroOrientation[1] * event.values[0]
|
||||||
|
val z = motionGyroOrientation[2] * event.values[2]
|
||||||
|
RyujinxNative.instance.inputSetGyroData(x, y, z, controllerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
RyujinxNative.instance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId)
|
||||||
|
RyujinxNative.instance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFlushCompleted(sensor: Sensor?) {
|
||||||
|
}
|
||||||
|
}
|
@ -31,4 +31,14 @@ class NativeHelpers {
|
|||||||
external fun getProgressValue() : Float
|
external fun getProgressValue() : Float
|
||||||
external fun storeStringJava(string: String) : Long
|
external fun storeStringJava(string: String) : Long
|
||||||
external fun getStringJava(id: Long) : String
|
external fun getStringJava(id: Long) : String
|
||||||
|
external fun setIsInitialOrientationFlipped(isFlipped: Boolean)
|
||||||
|
external fun getUiHandlerRequestType() : Int
|
||||||
|
external fun getUiHandlerRequestTitle() : Long
|
||||||
|
external fun getUiHandlerRequestMessage() : Long
|
||||||
|
external fun getUiHandlerMinLength() : Int
|
||||||
|
external fun getUiHandlerMaxLength() : Int
|
||||||
|
external fun getUiHandlerKeyboardMode() : Int
|
||||||
|
external fun getUiHandlerRequestWatermark() : Long
|
||||||
|
external fun getUiHandlerRequestInitialText() : Long
|
||||||
|
external fun getUiHandlerRequestSubtitle() : Long
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
package org.ryujinx.android
|
package org.ryujinx.android
|
||||||
|
|
||||||
|
import android.view.InputDevice
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import org.ryujinx.android.viewmodels.QuickSettings
|
||||||
|
|
||||||
class PhysicalControllerManager(val activity: MainActivity) {
|
class PhysicalControllerManager(val activity: MainActivity) {
|
||||||
private var controllerId: Int = -1
|
private var controllerId: Int = -1
|
||||||
private var ryujinxNative: RyujinxNative = RyujinxNative.instance
|
private var ryujinxNative: RyujinxNative = RyujinxNative.instance
|
||||||
|
|
||||||
fun onKeyEvent(event: KeyEvent) : Boolean{
|
fun onKeyEvent(event: KeyEvent) : Boolean{
|
||||||
if(controllerId != -1) {
|
|
||||||
val id = getGamePadButtonInputId(event.keyCode)
|
val id = getGamePadButtonInputId(event.keyCode)
|
||||||
|
|
||||||
if(id != GamePadButtonInputId.None) {
|
if(id != GamePadButtonInputId.None) {
|
||||||
|
val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0
|
||||||
|
if (/*controllerId != -1 &&*/ isNotFallback) {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
KeyEvent.ACTION_UP -> {
|
KeyEvent.ACTION_UP -> {
|
||||||
ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
|
ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
|
||||||
@ -23,13 +25,16 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
else if(!isNotFallback){
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMotionEvent(ev: MotionEvent) {
|
fun onMotionEvent(ev: MotionEvent) {
|
||||||
if(controllerId != -1) {
|
if(true) {
|
||||||
if(ev.action == MotionEvent.ACTION_MOVE) {
|
if(ev.action == MotionEvent.ACTION_MOVE) {
|
||||||
val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
|
val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
|
||||||
val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
|
val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
|
||||||
@ -37,20 +42,60 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
|||||||
val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
|
val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
|
||||||
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
|
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
|
||||||
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
|
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId)
|
||||||
|
|
||||||
|
ev.device?.apply {
|
||||||
|
if(sources and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD){
|
||||||
|
// Controller uses HAT
|
||||||
|
val dPadHor = ev.getAxisValue(MotionEvent.AXIS_HAT_X)
|
||||||
|
val dPadVert = ev.getAxisValue(MotionEvent.AXIS_HAT_Y)
|
||||||
|
if(dPadVert == 0.0f){
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId)
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId)
|
||||||
|
}
|
||||||
|
if(dPadHor == 0.0f){
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId)
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dPadVert < 0.0f){
|
||||||
|
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, controllerId)
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId)
|
||||||
|
}
|
||||||
|
if(dPadHor < 0.0f){
|
||||||
|
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, controllerId)
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dPadVert > 0.0f){
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId)
|
||||||
|
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, controllerId)
|
||||||
|
}
|
||||||
|
if(dPadHor > 0.0f){
|
||||||
|
ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId)
|
||||||
|
ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, controllerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connect(){
|
fun connect() : Int {
|
||||||
controllerId = ryujinxNative.inputConnectGamepad(0)
|
controllerId = ryujinxNative.inputConnectGamepad(0)
|
||||||
|
return controllerId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect(){
|
||||||
|
controllerId = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
||||||
|
val quickSettings = QuickSettings(activity)
|
||||||
return when (keycode) {
|
return when (keycode) {
|
||||||
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
|
KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B
|
||||||
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
|
KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A
|
||||||
KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X
|
KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y
|
||||||
KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y
|
KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X
|
||||||
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
|
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
|
||||||
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
|
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
|
||||||
KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder
|
KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.ryujinx.android
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class RyujinxApplication : Application() {
|
||||||
|
init {
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
|
||||||
|
companion object {
|
||||||
|
lateinit var instance : RyujinxApplication
|
||||||
|
private set
|
||||||
|
|
||||||
|
val context : Context get() = instance.applicationContext
|
||||||
|
}
|
||||||
|
}
|
@ -4,16 +4,18 @@ import org.ryujinx.android.viewmodels.GameInfo
|
|||||||
|
|
||||||
@Suppress("KotlinJniMissingFunction")
|
@Suppress("KotlinJniMissingFunction")
|
||||||
class RyujinxNative {
|
class RyujinxNative {
|
||||||
external fun initialize(appPath: Long, enableDebugLogs : Boolean): Boolean
|
external fun initialize(appPath: Long): Boolean
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance: RyujinxNative = RyujinxNative()
|
val instance: RyujinxNative = RyujinxNative()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("ryujinx")
|
System.loadLibrary("ryujinx")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean,
|
external fun deviceInitialize(
|
||||||
|
isHostMapped: Boolean, useNce: Boolean,
|
||||||
systemLanguage: Int,
|
systemLanguage: Int,
|
||||||
regionCode: Int,
|
regionCode: Int,
|
||||||
enableVsync: Boolean,
|
enableVsync: Boolean,
|
||||||
@ -21,7 +23,9 @@ class RyujinxNative {
|
|||||||
enablePtc: Boolean,
|
enablePtc: Boolean,
|
||||||
enableInternetAccess: Boolean,
|
enableInternetAccess: Boolean,
|
||||||
timeZone: Long,
|
timeZone: Long,
|
||||||
ignoreMissingServices : Boolean): Boolean
|
ignoreMissingServices: Boolean
|
||||||
|
): Boolean
|
||||||
|
|
||||||
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
||||||
external fun graphicsInitializeRenderer(
|
external fun graphicsInitializeRenderer(
|
||||||
extensions: Array<String>,
|
extensions: Array<String>,
|
||||||
@ -33,9 +37,9 @@ class RyujinxNative {
|
|||||||
external fun deviceGetGameFrameRate(): Double
|
external fun deviceGetGameFrameRate(): Double
|
||||||
external fun deviceGetGameFrameTime(): Double
|
external fun deviceGetGameFrameTime(): Double
|
||||||
external fun deviceGetGameFifo(): 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 deviceGetGameInfoFromPath(path: String): GameInfo
|
||||||
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
|
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean
|
||||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||||
external fun graphicsRendererRunLoop()
|
external fun graphicsRendererRunLoop()
|
||||||
@ -49,6 +53,8 @@ class RyujinxNative {
|
|||||||
external fun inputSetButtonReleased(button: Int, id: Int)
|
external fun inputSetButtonReleased(button: Int, id: Int)
|
||||||
external fun inputConnectGamepad(index: Int): Int
|
external fun inputConnectGamepad(index: Int): Int
|
||||||
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int)
|
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int)
|
||||||
|
external fun inputSetAccelerometerData(x: Float, y: Float, z: Float, id: Int)
|
||||||
|
external fun inputSetGyroData(x: Float, y: Float, z: Float, id: Int)
|
||||||
external fun graphicsSetSurface(surface: Long, window: Long)
|
external fun graphicsSetSurface(surface: Long, window: Long)
|
||||||
external fun deviceCloseEmulation()
|
external fun deviceCloseEmulation()
|
||||||
external fun deviceSignalEmulationClose()
|
external fun deviceSignalEmulationClose()
|
||||||
@ -64,4 +70,12 @@ class RyujinxNative {
|
|||||||
external fun userDeleteUser(userId: String)
|
external fun userDeleteUser(userId: String)
|
||||||
external fun userOpenUser(userId: Long)
|
external fun userOpenUser(userId: Long)
|
||||||
external fun userCloseUser(userId: String)
|
external fun userCloseUser(userId: String)
|
||||||
|
external fun loggingSetEnabled(logLevel: Int, enabled: Boolean)
|
||||||
|
external fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): Long
|
||||||
|
external fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean)
|
||||||
|
external fun deviceGetInstalledFirmwareVersion() : Long
|
||||||
|
external fun uiHandlerSetup()
|
||||||
|
external fun uiHandlerWait()
|
||||||
|
external fun uiHandlerStopWait()
|
||||||
|
external fun uiHandlerSetResponse(isOkPressed: Boolean, input: Long)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,215 @@
|
|||||||
|
package org.ryujinx.android
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.halilibo.richtext.markdown.Markdown
|
||||||
|
import com.halilibo.richtext.ui.RichText
|
||||||
|
|
||||||
|
internal enum class KeyboardMode {
|
||||||
|
Default, Numeric, ASCII, FullLatin, Alphabet, SimplifiedChinese, TraditionalChinese, Korean, LanguageSet2, LanguageSet2Latin
|
||||||
|
}
|
||||||
|
|
||||||
|
class UiHandler {
|
||||||
|
private var initialText: String = ""
|
||||||
|
private var subtitle: String = ""
|
||||||
|
private var maxLength: Int = 0
|
||||||
|
private var minLength: Int = 0
|
||||||
|
private var watermark: String = ""
|
||||||
|
private var type: Int = -1
|
||||||
|
private var mode: KeyboardMode = KeyboardMode.Default
|
||||||
|
val showMessage = mutableStateOf(false)
|
||||||
|
val inputText = mutableStateOf("")
|
||||||
|
var title: String = ""
|
||||||
|
var message: String = ""
|
||||||
|
var shouldListen = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
RyujinxNative.instance.uiHandlerSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listen() {
|
||||||
|
showMessage.value = false
|
||||||
|
while (shouldListen) {
|
||||||
|
RyujinxNative.instance.uiHandlerWait()
|
||||||
|
|
||||||
|
title =
|
||||||
|
NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestTitle())
|
||||||
|
message =
|
||||||
|
NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestMessage())
|
||||||
|
watermark =
|
||||||
|
NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestWatermark())
|
||||||
|
type = NativeHelpers.instance.getUiHandlerRequestType()
|
||||||
|
minLength = NativeHelpers.instance.getUiHandlerMinLength()
|
||||||
|
maxLength = NativeHelpers.instance.getUiHandlerMaxLength()
|
||||||
|
mode = KeyboardMode.values()[NativeHelpers.instance.getUiHandlerKeyboardMode()]
|
||||||
|
subtitle =
|
||||||
|
NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestSubtitle())
|
||||||
|
initialText =
|
||||||
|
NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestInitialText())
|
||||||
|
inputText.value = initialText
|
||||||
|
showMessage.value = type > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
shouldListen = false
|
||||||
|
RyujinxNative.instance.uiHandlerStopWait()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Compose() {
|
||||||
|
val showMessageListener = remember {
|
||||||
|
showMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputListener = remember {
|
||||||
|
inputText
|
||||||
|
}
|
||||||
|
val validation = remember {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validate() : Boolean{
|
||||||
|
if(inputText.value.isEmpty()){
|
||||||
|
validation.value = "Must be between ${minLength} and ${maxLength} characters"
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return inputText.value.length < minLength || inputText.value.length > maxLength
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInputType(): KeyboardType {
|
||||||
|
return when(mode){
|
||||||
|
KeyboardMode.Default -> KeyboardType.Text
|
||||||
|
KeyboardMode.Numeric -> KeyboardType.Decimal
|
||||||
|
KeyboardMode.ASCII -> KeyboardType.Ascii
|
||||||
|
else -> { KeyboardType.Text}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submit() {
|
||||||
|
var input: Long = -1
|
||||||
|
if (type == 2) {
|
||||||
|
if (inputListener.value.length < minLength || inputListener.value.length > maxLength)
|
||||||
|
return
|
||||||
|
input =
|
||||||
|
NativeHelpers.instance.storeStringJava(inputListener.value)
|
||||||
|
}
|
||||||
|
showMessageListener.value = false
|
||||||
|
RyujinxNative.instance.uiHandlerSetResponse(true, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMessageListener.value) {
|
||||||
|
AlertDialog(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
onDismissRequest = { },
|
||||||
|
properties = DialogProperties(dismissOnBackPress = false, false)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = title)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(128.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
RichText {
|
||||||
|
Markdown(content = message)
|
||||||
|
}
|
||||||
|
if (type == 2) {
|
||||||
|
validate()
|
||||||
|
if (watermark.isNotEmpty())
|
||||||
|
TextField(
|
||||||
|
value = inputListener.value,
|
||||||
|
onValueChange = { inputListener.value = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp),
|
||||||
|
label = {
|
||||||
|
Text(text = watermark)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = getInputType()),
|
||||||
|
isError = validate()
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextField(
|
||||||
|
value = inputListener.value,
|
||||||
|
onValueChange = { inputListener.value = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = getInputType(),
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
isError = validate(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardActions = KeyboardActions(onDone = { submit() })
|
||||||
|
)
|
||||||
|
if (subtitle.isNotEmpty())
|
||||||
|
Text(text = subtitle)
|
||||||
|
Text(text = validation.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
submit()
|
||||||
|
}) {
|
||||||
|
Text(text = "OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
|||||||
|
package org.ryujinx.android.providers
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.DocumentsProvider
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import org.ryujinx.android.BuildConfig
|
||||||
|
import org.ryujinx.android.R
|
||||||
|
import org.ryujinx.android.RyujinxApplication
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class DocumentProvider : DocumentsProvider() {
|
||||||
|
private val baseDirectory = File(RyujinxApplication.instance.getPublicFilesDir().canonicalPath)
|
||||||
|
private val applicationName = "Ryujinx"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEFAULT_ROOT_PROJECTION : Array<String> = arrayOf(
|
||||||
|
DocumentsContract.Root.COLUMN_ROOT_ID,
|
||||||
|
DocumentsContract.Root.COLUMN_MIME_TYPES,
|
||||||
|
DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Root.COLUMN_ICON,
|
||||||
|
DocumentsContract.Root.COLUMN_TITLE,
|
||||||
|
DocumentsContract.Root.COLUMN_SUMMARY,
|
||||||
|
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_DOCUMENT_PROJECTION : Array<String> = arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||||
|
DocumentsContract.Document.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
const val AUTHORITY : String = BuildConfig.APPLICATION_ID + ".providers"
|
||||||
|
|
||||||
|
const val ROOT_ID : String = "root"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() : Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The [File] that corresponds to the document ID supplied by [getDocumentId]
|
||||||
|
*/
|
||||||
|
private fun getFile(documentId : String) : File {
|
||||||
|
if (documentId.startsWith(ROOT_ID)) {
|
||||||
|
val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
|
||||||
|
if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found")
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
throw FileNotFoundException("'$documentId' is not in any known root")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A unique ID for the provided [File]
|
||||||
|
*/
|
||||||
|
private fun getDocumentId(file : File) : String {
|
||||||
|
return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryRoots(projection : Array<out String>?) : Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
|
||||||
|
|
||||||
|
cursor.newRow().apply {
|
||||||
|
add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
|
||||||
|
add(DocumentsContract.Root.COLUMN_SUMMARY, null)
|
||||||
|
add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD)
|
||||||
|
add(DocumentsContract.Root.COLUMN_TITLE, applicationName)
|
||||||
|
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
|
||||||
|
add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
|
||||||
|
add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
|
||||||
|
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryDocument(documentId : String?, projection : Array<out String>?) : Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
|
return includeFile(cursor, documentId, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isChildDocument(parentDocumentId : String?, documentId : String?) : Boolean {
|
||||||
|
return documentId?.startsWith(parentDocumentId!!) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
|
||||||
|
*/
|
||||||
|
fun File.resolveWithoutConflict(name : String) : File {
|
||||||
|
var file = resolve(name)
|
||||||
|
if (file.exists()) {
|
||||||
|
var noConflictId = 1 // Makes sure two files don't have the same name by adding a number to the end
|
||||||
|
val extension = name.substringAfterLast('.')
|
||||||
|
val baseName = name.substringBeforeLast('.')
|
||||||
|
while (file.exists())
|
||||||
|
file = resolve("$baseName (${noConflictId++}).$extension")
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDocument(parentDocumentId : String?, mimeType : String?, displayName : String) : String? {
|
||||||
|
val parentFile = getFile(parentDocumentId!!)
|
||||||
|
val newFile = parentFile.resolveWithoutConflict(displayName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
|
||||||
|
if (!newFile.mkdir())
|
||||||
|
throw IOException("Failed to create directory")
|
||||||
|
} else {
|
||||||
|
if (!newFile.createNewFile())
|
||||||
|
throw IOException("Failed to create file")
|
||||||
|
}
|
||||||
|
} catch (e : IOException) {
|
||||||
|
throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDocumentId(newFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteDocument(documentId : String?) {
|
||||||
|
val file = getFile(documentId!!)
|
||||||
|
if (!file.delete())
|
||||||
|
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeDocument(documentId : String, parentDocumentId : String?) {
|
||||||
|
val parent = getFile(parentDocumentId!!)
|
||||||
|
val file = getFile(documentId)
|
||||||
|
|
||||||
|
if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
|
||||||
|
if (!file.delete())
|
||||||
|
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
|
||||||
|
} else {
|
||||||
|
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renameDocument(documentId : String?, displayName : String?) : String? {
|
||||||
|
if (displayName == null)
|
||||||
|
throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null")
|
||||||
|
|
||||||
|
val sourceFile = getFile(documentId!!)
|
||||||
|
val sourceParentFile = sourceFile.parentFile ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent")
|
||||||
|
val destFile = sourceParentFile.resolve(displayName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!sourceFile.renameTo(destFile))
|
||||||
|
throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'")
|
||||||
|
} catch (e : Exception) {
|
||||||
|
throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDocumentId(destFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyDocument(
|
||||||
|
sourceDocumentId : String, sourceParentDocumentId : String,
|
||||||
|
targetParentDocumentId : String?
|
||||||
|
) : String? {
|
||||||
|
if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
|
||||||
|
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'")
|
||||||
|
|
||||||
|
return copyDocument(sourceDocumentId, targetParentDocumentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun copyDocument(sourceDocumentId : String, targetParentDocumentId : String?) : String? {
|
||||||
|
val parent = getFile(targetParentDocumentId!!)
|
||||||
|
val oldFile = getFile(sourceDocumentId)
|
||||||
|
val newFile = parent.resolveWithoutConflict(oldFile.name)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
|
||||||
|
throw IOException("Couldn't create new file")
|
||||||
|
|
||||||
|
FileInputStream(oldFile).use { inStream ->
|
||||||
|
FileOutputStream(newFile).use { outStream ->
|
||||||
|
inStream.copyTo(outStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e : IOException) {
|
||||||
|
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDocumentId(newFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun moveDocument(
|
||||||
|
sourceDocumentId : String, sourceParentDocumentId : String?,
|
||||||
|
targetParentDocumentId : String?
|
||||||
|
) : String? {
|
||||||
|
try {
|
||||||
|
val newDocumentId = copyDocument(
|
||||||
|
sourceDocumentId, sourceParentDocumentId!!,
|
||||||
|
targetParentDocumentId
|
||||||
|
)
|
||||||
|
removeDocument(sourceDocumentId, sourceParentDocumentId)
|
||||||
|
return newDocumentId
|
||||||
|
} catch (e : FileNotFoundException) {
|
||||||
|
throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun includeFile(cursor : MatrixCursor, documentId : String?, file : File?) : MatrixCursor {
|
||||||
|
val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
|
||||||
|
val localFile = file ?: getFile(documentId!!)
|
||||||
|
|
||||||
|
var flags = 0
|
||||||
|
if (localFile.isDirectory && localFile.canWrite()) {
|
||||||
|
flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
|
||||||
|
} else if (localFile.canWrite()) {
|
||||||
|
flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
|
||||||
|
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.newRow().apply {
|
||||||
|
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
|
||||||
|
add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if (localFile == baseDirectory) applicationName else localFile.name)
|
||||||
|
add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
|
||||||
|
add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
|
||||||
|
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
|
||||||
|
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
|
||||||
|
if (localFile == baseDirectory)
|
||||||
|
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTypeForFile(file : File) : Any? {
|
||||||
|
return if (file.isDirectory)
|
||||||
|
DocumentsContract.Document.MIME_TYPE_DIR
|
||||||
|
else
|
||||||
|
getTypeForName(file.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTypeForName(name : String) : Any? {
|
||||||
|
val lastDot = name.lastIndexOf('.')
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
val extension = name.substring(lastDot + 1)
|
||||||
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
|
if (mime != null)
|
||||||
|
return mime
|
||||||
|
}
|
||||||
|
return "application/octect-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryChildDocuments(parentDocumentId : String?, projection : Array<out String>?, sortOrder : String?) : Cursor {
|
||||||
|
var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
|
|
||||||
|
val parent = getFile(parentDocumentId!!)
|
||||||
|
for (file in parent.listFiles()!!)
|
||||||
|
cursor = includeFile(cursor, null, file)
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openDocument(documentId : String?, mode : String?, signal : CancellationSignal?) : ParcelFileDescriptor {
|
||||||
|
val file = documentId?.let { getFile(it) }
|
||||||
|
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||||
|
return ParcelFileDescriptor.open(file, accessMode)
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@ class DlcViewModel(val titleId: String) {
|
|||||||
val path = file.getAbsolutePath(storageHelper.storage.context)
|
val path = file.getAbsolutePath(storageHelper.storage.context)
|
||||||
if (path.isNotEmpty()) {
|
if (path.isNotEmpty()) {
|
||||||
data?.apply {
|
data?.apply {
|
||||||
var contents = RyujinxNative.instance.deviceGetDlcContentList(
|
val contents = RyujinxNative.instance.deviceGetDlcContentList(
|
||||||
NativeHelpers.instance.storeStringJava(path),
|
NativeHelpers.instance.storeStringJava(path),
|
||||||
titleId.toLong(16)
|
titleId.toLong(16)
|
||||||
)
|
)
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
package org.ryujinx.android.viewmodels
|
package org.ryujinx.android.viewmodels
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.anggrayudi.storage.file.extension
|
import com.anggrayudi.storage.file.extension
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.RyujinxNative
|
import org.ryujinx.android.RyujinxNative
|
||||||
|
|
||||||
|
|
||||||
class GameModel(var file: DocumentFile, val context: Context) {
|
class GameModel(var file: DocumentFile, val context: Context) {
|
||||||
|
private var updateDescriptor: ParcelFileDescriptor? = null
|
||||||
|
var type: FileType
|
||||||
var descriptor: ParcelFileDescriptor? = null
|
var descriptor: ParcelFileDescriptor? = null
|
||||||
var fileName: String?
|
var fileName: String?
|
||||||
var fileSize = 0.0
|
var fileSize = 0.0
|
||||||
@ -19,8 +23,9 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
fileName = file.name
|
fileName = file.name
|
||||||
var pid = open()
|
val pid = open()
|
||||||
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci"))
|
val ext = NativeHelpers.instance.storeStringJava(file.extension)
|
||||||
|
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext)
|
||||||
close()
|
close()
|
||||||
|
|
||||||
fileSize = gameInfo.FileSize
|
fileSize = gameInfo.FileSize
|
||||||
@ -29,6 +34,16 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
|||||||
developer = gameInfo.Developer
|
developer = gameInfo.Developer
|
||||||
version = gameInfo.Version
|
version = gameInfo.Version
|
||||||
icon = gameInfo.Icon
|
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 {
|
fun open() : Int {
|
||||||
@ -37,13 +52,29 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
|||||||
return descriptor?.fd ?: 0
|
return descriptor?.fd ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openUpdate() : Int {
|
||||||
|
if(titleId?.isNotEmpty() == true) {
|
||||||
|
val vm = TitleUpdateViewModel(titleId ?: "")
|
||||||
|
|
||||||
|
if(vm.data?.selected?.isNotEmpty() == true){
|
||||||
|
val uri = Uri.parse(vm.data?.selected)
|
||||||
|
val file = DocumentFile.fromSingleUri(context, uri)
|
||||||
|
if(file?.exists() == true){
|
||||||
|
updateDescriptor = context.contentResolver.openFileDescriptor(file.uri, "rw")
|
||||||
|
|
||||||
|
return updateDescriptor ?.fd ?: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
descriptor?.close()
|
descriptor?.close()
|
||||||
descriptor = null
|
descriptor = null
|
||||||
}
|
updateDescriptor?.close()
|
||||||
|
updateDescriptor = null
|
||||||
fun isXci() : Boolean {
|
|
||||||
return file.extension == "xci"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,3 +86,10 @@ class GameInfo {
|
|||||||
var Version: String? = null
|
var Version: String? = null
|
||||||
var Icon: String? = null
|
var Icon: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class FileType{
|
||||||
|
None,
|
||||||
|
Nsp,
|
||||||
|
Xci,
|
||||||
|
Nro
|
||||||
|
}
|
||||||
|
@ -6,19 +6,20 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||||
import com.anggrayudi.storage.file.DocumentFileType
|
import com.anggrayudi.storage.file.DocumentFileType
|
||||||
import com.anggrayudi.storage.file.FileFullPath
|
|
||||||
import com.anggrayudi.storage.file.extension
|
import com.anggrayudi.storage.file.extension
|
||||||
import com.anggrayudi.storage.file.getAbsolutePath
|
|
||||||
import com.anggrayudi.storage.file.search
|
import com.anggrayudi.storage.file.search
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
val activity: MainActivity? = null,
|
val activity: MainActivity? = null,
|
||||||
val mainViewModel: MainViewModel? = null
|
val mainViewModel: MainViewModel? = null
|
||||||
) {
|
) {
|
||||||
|
private var shouldReload: Boolean = false
|
||||||
|
private var savedFolder: String = ""
|
||||||
private var isLoading: Boolean = false
|
private var isLoading: Boolean = false
|
||||||
private var loadedCache: List<GameModel> = listOf()
|
private var loadedCache: MutableList<GameModel> = mutableListOf()
|
||||||
private var gameFolderPath: DocumentFile? = null
|
private var gameFolderPath: DocumentFile? = null
|
||||||
private var sharedPref: SharedPreferences? = null
|
private var sharedPref: SharedPreferences? = null
|
||||||
val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
|
val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
|
||||||
@ -26,46 +27,34 @@ class HomeViewModel(
|
|||||||
init {
|
init {
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
sharedPref = PreferenceManager.getDefaultSharedPreferences(activity)
|
sharedPref = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
|
|
||||||
run {
|
|
||||||
gameFolderPath = folder
|
|
||||||
val p = folder.getAbsolutePath(activity!!)
|
|
||||||
val editor = sharedPref?.edit()
|
|
||||||
editor?.putString("gameFolder", p)
|
|
||||||
editor?.apply()
|
|
||||||
reloadGameList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
|
fun ensureReloadIfNecessary() {
|
||||||
|
val oldFolder = savedFolder
|
||||||
|
savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
|
||||||
|
|
||||||
if (savedFolder.isNotEmpty()) {
|
if (savedFolder.isNotEmpty() && (shouldReload || savedFolder != oldFolder)) {
|
||||||
try {
|
|
||||||
gameFolderPath = DocumentFileCompat.fromFullPath(
|
gameFolderPath = DocumentFileCompat.fromFullPath(
|
||||||
activity,
|
mainViewModel?.activity!!,
|
||||||
savedFolder,
|
savedFolder,
|
||||||
documentType = DocumentFileType.FOLDER,
|
documentType = DocumentFileType.FOLDER,
|
||||||
requiresWriteAccess = true
|
requiresWriteAccess = true
|
||||||
)
|
)
|
||||||
|
|
||||||
reloadGameList()
|
reloadGameList()
|
||||||
} catch (e: Exception) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openGameFolder() {
|
fun filter(query : String){
|
||||||
val path = sharedPref?.getString("gameFolder", "") ?: ""
|
gameList.clear()
|
||||||
|
gameList.addAll(loadedCache.filter { it.titleName != null && it.titleName!!.isNotEmpty() && (query.trim()
|
||||||
|
.isEmpty() || it.titleName!!.lowercase(Locale.getDefault())
|
||||||
|
.contains(query)) })
|
||||||
|
}
|
||||||
|
|
||||||
if (path.isEmpty())
|
fun requestReload(){
|
||||||
activity?.storageHelper?.storage?.openFolderPicker()
|
shouldReload = true
|
||||||
else
|
|
||||||
activity?.storageHelper?.storage?.openFolderPicker(
|
|
||||||
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
|
||||||
FileFullPath(activity, path)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGameList() {
|
fun reloadGameList() {
|
||||||
@ -80,17 +69,19 @@ class HomeViewModel(
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
|
loadedCache.clear()
|
||||||
val files = mutableListOf<GameModel>()
|
val files = mutableListOf<GameModel>()
|
||||||
for (file in folder.search(false, DocumentFileType.FILE)) {
|
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 {
|
activity.let {
|
||||||
val item = GameModel(file, it)
|
val item = GameModel(file, it)
|
||||||
files.add(item)
|
|
||||||
|
if(item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true) {
|
||||||
|
loadedCache.add(item)
|
||||||
gameList.add(item)
|
gameList.add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
loadedCache = files.toList()
|
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
} finally {
|
} finally {
|
||||||
@ -98,8 +89,4 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearLoadedCache(){
|
|
||||||
loadedCache = listOf()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,9 @@ import kotlinx.coroutines.sync.Semaphore
|
|||||||
import org.ryujinx.android.GameController
|
import org.ryujinx.android.GameController
|
||||||
import org.ryujinx.android.GameHost
|
import org.ryujinx.android.GameHost
|
||||||
import org.ryujinx.android.GraphicsConfiguration
|
import org.ryujinx.android.GraphicsConfiguration
|
||||||
|
import org.ryujinx.android.Logging
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import org.ryujinx.android.MotionSensorManager
|
||||||
import org.ryujinx.android.NativeGraphicsInterop
|
import org.ryujinx.android.NativeGraphicsInterop
|
||||||
import org.ryujinx.android.NativeHelpers
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.PerformanceManager
|
import org.ryujinx.android.PerformanceManager
|
||||||
@ -25,12 +27,15 @@ import java.io.File
|
|||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
class MainViewModel(val activity: MainActivity) {
|
class MainViewModel(val activity: MainActivity) {
|
||||||
var physicalControllerManager: PhysicalControllerManager? = null
|
var physicalControllerManager: PhysicalControllerManager? = null
|
||||||
|
var motionSensorManager: MotionSensorManager? = null
|
||||||
var gameModel: GameModel? = null
|
var gameModel: GameModel? = null
|
||||||
var controller: GameController? = null
|
var controller: GameController? = null
|
||||||
var performanceManager: PerformanceManager? = null
|
var performanceManager: PerformanceManager? = null
|
||||||
var selected: GameModel? = null
|
var selected: GameModel? = null
|
||||||
var isMiiEditorLaunched = false
|
var isMiiEditorLaunched = false
|
||||||
val userViewModel = UserViewModel()
|
val userViewModel = UserViewModel()
|
||||||
|
val logging = Logging(this)
|
||||||
|
var firmwareVersion = ""
|
||||||
private var gameTimeState: MutableState<Double>? = null
|
private var gameTimeState: MutableState<Double>? = null
|
||||||
private var gameFpsState: MutableState<Double>? = null
|
private var gameFpsState: MutableState<Double>? = null
|
||||||
private var fifoState: MutableState<Double>? = null
|
private var fifoState: MutableState<Double>? = null
|
||||||
@ -38,6 +43,7 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
private var progressValue: MutableState<Float>? = null
|
private var progressValue: MutableState<Float>? = null
|
||||||
private var showLoading: MutableState<Boolean>? = null
|
private var showLoading: MutableState<Boolean>? = null
|
||||||
private var refreshUser: MutableState<Boolean>? = null
|
private var refreshUser: MutableState<Boolean>? = null
|
||||||
|
|
||||||
var gameHost: GameHost? = null
|
var gameHost: GameHost? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@ -59,6 +65,16 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
RyujinxNative.instance.deviceSignalEmulationClose()
|
RyujinxNative.instance.deviceSignalEmulationClose()
|
||||||
gameHost?.close()
|
gameHost?.close()
|
||||||
RyujinxNative.instance.deviceCloseEmulation()
|
RyujinxNative.instance.deviceCloseEmulation()
|
||||||
|
motionSensorManager?.unregister()
|
||||||
|
physicalControllerManager?.disconnect()
|
||||||
|
motionSensorManager?.setControllerId(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshFirmwareVersion(){
|
||||||
|
var handle = RyujinxNative.instance.deviceGetInstalledFirmwareVersion()
|
||||||
|
if(handle != -1L) {
|
||||||
|
firmwareVersion = NativeHelpers.instance.getStringJava(handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadGame(game:GameModel) : Boolean {
|
fun loadGame(game:GameModel) : Boolean {
|
||||||
@ -69,6 +85,8 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
if (descriptor == 0)
|
if (descriptor == 0)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
val update = game.openUpdate()
|
||||||
|
|
||||||
gameModel = game
|
gameModel = game
|
||||||
isMiiEditorLaunched = false
|
isMiiEditorLaunched = false
|
||||||
|
|
||||||
@ -162,7 +180,7 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
if (!success)
|
if (!success)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
|
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
return false
|
return false
|
||||||
@ -170,8 +188,6 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun loadMiiEditor() : Boolean {
|
fun loadMiiEditor() : Boolean {
|
||||||
val nativeRyujinx = RyujinxNative.instance
|
val nativeRyujinx = RyujinxNative.instance
|
||||||
|
|
||||||
@ -351,6 +367,8 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
activity.setFullScreen(true)
|
activity.setFullScreen(true)
|
||||||
navController?.navigate("game")
|
navController?.navigate("game")
|
||||||
activity.isGameRunning = true
|
activity.isGameRunning = true
|
||||||
|
if (QuickSettings(activity).enableMotion)
|
||||||
|
motionSensorManager?.register()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressStates(
|
fun setProgressStates(
|
||||||
|
@ -15,6 +15,19 @@ class QuickSettings(val activity: Activity) {
|
|||||||
var enableShaderCache: Boolean
|
var enableShaderCache: Boolean
|
||||||
var enableTextureRecompression: Boolean
|
var enableTextureRecompression: Boolean
|
||||||
var resScale : Float
|
var resScale : Float
|
||||||
|
var isGrid : Boolean
|
||||||
|
var useSwitchLayout : Boolean
|
||||||
|
var enableMotion : Boolean
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
var enableDebugLogs: Boolean
|
||||||
|
var enableStubLogs: Boolean
|
||||||
|
var enableInfoLogs: Boolean
|
||||||
|
var enableWarningLogs: Boolean
|
||||||
|
var enableErrorLogs: Boolean
|
||||||
|
var enableGuestLogs: Boolean
|
||||||
|
var enableAccessLogs: Boolean
|
||||||
|
var enableTraceLogs: Boolean
|
||||||
|
|
||||||
private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
|
||||||
@ -29,5 +42,46 @@ class QuickSettings(val activity: Activity) {
|
|||||||
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
|
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
|
||||||
resScale = sharedPref.getFloat("resScale", 1f)
|
resScale = sharedPref.getFloat("resScale", 1f)
|
||||||
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
|
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
|
||||||
|
isGrid = sharedPref.getBoolean("isGrid", true)
|
||||||
|
useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true)
|
||||||
|
enableMotion = sharedPref.getBoolean("enableMotion", true)
|
||||||
|
|
||||||
|
enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false)
|
||||||
|
enableStubLogs = sharedPref.getBoolean("enableStubLogs", false)
|
||||||
|
enableInfoLogs = sharedPref.getBoolean("enableInfoLogs", true)
|
||||||
|
enableWarningLogs = sharedPref.getBoolean("enableWarningLogs", true)
|
||||||
|
enableErrorLogs = sharedPref.getBoolean("enableErrorLogs", true)
|
||||||
|
enableGuestLogs = sharedPref.getBoolean("enableGuestLogs", true)
|
||||||
|
enableAccessLogs = sharedPref.getBoolean("enableAccessLogs", false)
|
||||||
|
enableTraceLogs = sharedPref.getBoolean("enableStubLogs", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(){
|
||||||
|
val editor = sharedPref.edit()
|
||||||
|
|
||||||
|
editor.putBoolean("isHostMapped", isHostMapped)
|
||||||
|
editor.putBoolean("useNce", useNce)
|
||||||
|
editor.putBoolean("enableVsync", enableVsync)
|
||||||
|
editor.putBoolean("enableDocked", enableDocked)
|
||||||
|
editor.putBoolean("enablePtc", enablePtc)
|
||||||
|
editor.putBoolean("ignoreMissingServices", ignoreMissingServices)
|
||||||
|
editor.putBoolean("enableShaderCache", enableShaderCache)
|
||||||
|
editor.putBoolean("enableTextureRecompression", enableTextureRecompression)
|
||||||
|
editor.putFloat("resScale", resScale)
|
||||||
|
editor.putBoolean("useVirtualController", useVirtualController)
|
||||||
|
editor.putBoolean("isGrid", isGrid)
|
||||||
|
editor.putBoolean("useSwitchLayout", useSwitchLayout)
|
||||||
|
editor.putBoolean("enableMotion", enableMotion)
|
||||||
|
|
||||||
|
editor.putBoolean("enableDebugLogs", enableDebugLogs)
|
||||||
|
editor.putBoolean("enableStubLogs", enableStubLogs)
|
||||||
|
editor.putBoolean("enableInfoLogs", enableInfoLogs)
|
||||||
|
editor.putBoolean("enableWarningLogs", enableWarningLogs)
|
||||||
|
editor.putBoolean("enableErrorLogs", enableErrorLogs)
|
||||||
|
editor.putBoolean("enableGuestLogs", enableGuestLogs)
|
||||||
|
editor.putBoolean("enableAccessLogs", enableAccessLogs)
|
||||||
|
editor.putBoolean("enableTraceLogs", enableTraceLogs)
|
||||||
|
|
||||||
|
editor.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,40 @@ package org.ryujinx.android.viewmodels
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.anggrayudi.storage.callback.FileCallback
|
||||||
|
import com.anggrayudi.storage.file.FileFullPath
|
||||||
|
import com.anggrayudi.storage.file.copyFileTo
|
||||||
|
import com.anggrayudi.storage.file.extension
|
||||||
|
import com.anggrayudi.storage.file.getAbsolutePath
|
||||||
|
import org.ryujinx.android.LogLevel
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
|
import org.ryujinx.android.RyujinxNative
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) {
|
class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) {
|
||||||
|
var selectedFirmwareVersion: String = ""
|
||||||
|
private var previousFileCallback: ((requestCode: Int, files: List<DocumentFile>) -> Unit)?
|
||||||
|
private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
|
||||||
private var sharedPref: SharedPreferences
|
private var sharedPref: SharedPreferences
|
||||||
|
var selectedFirmwareFile: DocumentFile? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sharedPref = getPreferences()
|
sharedPref = getPreferences()
|
||||||
|
previousFolderCallback = activity.storageHelper!!.onFolderSelected
|
||||||
|
previousFileCallback = activity.storageHelper!!.onFileSelected
|
||||||
|
activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
|
||||||
|
run {
|
||||||
|
val p = folder.getAbsolutePath(activity)
|
||||||
|
val editor = sharedPref.edit()
|
||||||
|
editor?.putString("gameFolder", p)
|
||||||
|
editor?.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPreferences(): SharedPreferences {
|
private fun getPreferences(): SharedPreferences {
|
||||||
@ -27,9 +52,19 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
enableShaderCache: MutableState<Boolean>,
|
enableShaderCache: MutableState<Boolean>,
|
||||||
enableTextureRecompression: MutableState<Boolean>,
|
enableTextureRecompression: MutableState<Boolean>,
|
||||||
resScale: MutableState<Float>,
|
resScale: MutableState<Float>,
|
||||||
useVirtualController: MutableState<Boolean>
|
useVirtualController: MutableState<Boolean>,
|
||||||
)
|
isGrid: MutableState<Boolean>,
|
||||||
{
|
useSwitchLayout: MutableState<Boolean>,
|
||||||
|
enableMotion: MutableState<Boolean>,
|
||||||
|
enableDebugLogs: MutableState<Boolean>,
|
||||||
|
enableStubLogs: MutableState<Boolean>,
|
||||||
|
enableInfoLogs: MutableState<Boolean>,
|
||||||
|
enableWarningLogs: MutableState<Boolean>,
|
||||||
|
enableErrorLogs: MutableState<Boolean>,
|
||||||
|
enableGuestLogs: MutableState<Boolean>,
|
||||||
|
enableAccessLogs: MutableState<Boolean>,
|
||||||
|
enableTraceLogs: MutableState<Boolean>
|
||||||
|
) {
|
||||||
|
|
||||||
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
|
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
|
||||||
useNce.value = sharedPref.getBoolean("useNce", true)
|
useNce.value = sharedPref.getBoolean("useNce", true)
|
||||||
@ -38,9 +73,22 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
enablePtc.value = sharedPref.getBoolean("enablePtc", true)
|
enablePtc.value = sharedPref.getBoolean("enablePtc", true)
|
||||||
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
|
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
|
||||||
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
|
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
|
||||||
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
|
enableTextureRecompression.value =
|
||||||
|
sharedPref.getBoolean("enableTextureRecompression", false)
|
||||||
resScale.value = sharedPref.getFloat("resScale", 1f)
|
resScale.value = sharedPref.getFloat("resScale", 1f)
|
||||||
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
|
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
|
||||||
|
isGrid.value = sharedPref.getBoolean("isGrid", true)
|
||||||
|
useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true)
|
||||||
|
enableMotion.value = sharedPref.getBoolean("enableMotion", true)
|
||||||
|
|
||||||
|
enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false)
|
||||||
|
enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false)
|
||||||
|
enableInfoLogs.value = sharedPref.getBoolean("enableInfoLogs", true)
|
||||||
|
enableWarningLogs.value = sharedPref.getBoolean("enableWarningLogs", true)
|
||||||
|
enableErrorLogs.value = sharedPref.getBoolean("enableErrorLogs", true)
|
||||||
|
enableGuestLogs.value = sharedPref.getBoolean("enableGuestLogs", true)
|
||||||
|
enableAccessLogs.value = sharedPref.getBoolean("enableAccessLogs", false)
|
||||||
|
enableTraceLogs.value = sharedPref.getBoolean("enableStubLogs", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun save(
|
fun save(
|
||||||
@ -53,7 +101,18 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
enableShaderCache: MutableState<Boolean>,
|
enableShaderCache: MutableState<Boolean>,
|
||||||
enableTextureRecompression: MutableState<Boolean>,
|
enableTextureRecompression: MutableState<Boolean>,
|
||||||
resScale: MutableState<Float>,
|
resScale: MutableState<Float>,
|
||||||
useVirtualController: MutableState<Boolean>
|
useVirtualController: MutableState<Boolean>,
|
||||||
|
isGrid: MutableState<Boolean>,
|
||||||
|
useSwitchLayout: MutableState<Boolean>,
|
||||||
|
enableMotion: MutableState<Boolean>,
|
||||||
|
enableDebugLogs: MutableState<Boolean>,
|
||||||
|
enableStubLogs: MutableState<Boolean>,
|
||||||
|
enableInfoLogs: MutableState<Boolean>,
|
||||||
|
enableWarningLogs: MutableState<Boolean>,
|
||||||
|
enableErrorLogs: MutableState<Boolean>,
|
||||||
|
enableGuestLogs: MutableState<Boolean>,
|
||||||
|
enableAccessLogs: MutableState<Boolean>,
|
||||||
|
enableTraceLogs: MutableState<Boolean>
|
||||||
) {
|
) {
|
||||||
val editor = sharedPref.edit()
|
val editor = sharedPref.edit()
|
||||||
|
|
||||||
@ -67,7 +126,148 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
|
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
|
||||||
editor.putFloat("resScale", resScale.value)
|
editor.putFloat("resScale", resScale.value)
|
||||||
editor.putBoolean("useVirtualController", useVirtualController.value)
|
editor.putBoolean("useVirtualController", useVirtualController.value)
|
||||||
|
editor.putBoolean("isGrid", isGrid.value)
|
||||||
|
editor.putBoolean("useSwitchLayout", useSwitchLayout.value)
|
||||||
|
editor.putBoolean("enableMotion", enableMotion.value)
|
||||||
|
|
||||||
|
editor.putBoolean("enableDebugLogs", enableDebugLogs.value)
|
||||||
|
editor.putBoolean("enableStubLogs", enableStubLogs.value)
|
||||||
|
editor.putBoolean("enableInfoLogs", enableInfoLogs.value)
|
||||||
|
editor.putBoolean("enableWarningLogs", enableWarningLogs.value)
|
||||||
|
editor.putBoolean("enableErrorLogs", enableErrorLogs.value)
|
||||||
|
editor.putBoolean("enableGuestLogs", enableGuestLogs.value)
|
||||||
|
editor.putBoolean("enableAccessLogs", enableAccessLogs.value)
|
||||||
|
editor.putBoolean("enableTraceLogs", enableTraceLogs.value)
|
||||||
|
|
||||||
editor.apply()
|
editor.apply()
|
||||||
|
activity.storageHelper!!.onFolderSelected = previousFolderCallback
|
||||||
|
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Stub.ordinal, enableStubLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Warning.ordinal, enableWarningLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Error.ordinal, enableErrorLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.AccessLog.ordinal, enableAccessLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Guest.ordinal, enableGuestLogs.value)
|
||||||
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openGameFolder() {
|
||||||
|
val path = sharedPref?.getString("gameFolder", "") ?: ""
|
||||||
|
|
||||||
|
if (path.isEmpty())
|
||||||
|
activity.storageHelper?.storage?.openFolderPicker()
|
||||||
|
else
|
||||||
|
activity.storageHelper?.storage?.openFolderPicker(
|
||||||
|
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
||||||
|
FileFullPath(activity, path)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importProdKeys() {
|
||||||
|
activity.storageHelper!!.onFileSelected = { requestCode, files ->
|
||||||
|
run {
|
||||||
|
activity.storageHelper!!.onFileSelected = previousFileCallback
|
||||||
|
val file = files.firstOrNull()
|
||||||
|
file?.apply {
|
||||||
|
if (name == "prod.keys") {
|
||||||
|
val outputFile = File(MainActivity.AppPath + "/system");
|
||||||
|
outputFile.delete()
|
||||||
|
|
||||||
|
thread {
|
||||||
|
file.copyFileTo(
|
||||||
|
activity,
|
||||||
|
outputFile,
|
||||||
|
callback = object : FileCallback() {
|
||||||
|
override fun onCompleted(result: Any) {
|
||||||
|
super.onCompleted(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity.storageHelper?.storage?.openFilePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectFirmware(installState: MutableState<FirmwareInstallState>) {
|
||||||
|
if (installState.value != FirmwareInstallState.None)
|
||||||
|
return
|
||||||
|
activity.storageHelper!!.onFileSelected = { _, files ->
|
||||||
|
run {
|
||||||
|
activity.storageHelper!!.onFileSelected = previousFileCallback
|
||||||
|
val file = files.firstOrNull()
|
||||||
|
file?.apply {
|
||||||
|
if (extension == "xci" || extension == "zip") {
|
||||||
|
installState.value = FirmwareInstallState.Verifying
|
||||||
|
thread {
|
||||||
|
val descriptor =
|
||||||
|
activity.contentResolver.openFileDescriptor(file.uri, "rw")
|
||||||
|
descriptor?.use { d ->
|
||||||
|
val version = RyujinxNative.instance.deviceVerifyFirmware(
|
||||||
|
d.fd,
|
||||||
|
extension == "xci"
|
||||||
|
)
|
||||||
|
selectedFirmwareFile = file
|
||||||
|
if (version != -1L) {
|
||||||
|
selectedFirmwareVersion =
|
||||||
|
NativeHelpers.instance.getStringJava(version)
|
||||||
|
installState.value = FirmwareInstallState.Query
|
||||||
|
} else {
|
||||||
|
installState.value = FirmwareInstallState.Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installState.value = FirmwareInstallState.Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity.storageHelper?.storage?.openFilePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installFirmware(installState: MutableState<FirmwareInstallState>) {
|
||||||
|
if (installState.value != FirmwareInstallState.Query)
|
||||||
|
return
|
||||||
|
if (selectedFirmwareFile == null) {
|
||||||
|
installState.value = FirmwareInstallState.None
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedFirmwareFile?.apply {
|
||||||
|
val descriptor =
|
||||||
|
activity.contentResolver.openFileDescriptor(uri, "rw")
|
||||||
|
descriptor?.use { d ->
|
||||||
|
installState.value = FirmwareInstallState.Install
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
|
RyujinxNative.instance.deviceInstallFirmware(
|
||||||
|
d.fd,
|
||||||
|
extension == "xci"
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
MainActivity.mainViewModel?.refreshFirmwareVersion()
|
||||||
|
installState.value = FirmwareInstallState.Done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearFirmwareSelection(installState: MutableState<FirmwareInstallState>){
|
||||||
|
selectedFirmwareFile = null
|
||||||
|
selectedFirmwareVersion = ""
|
||||||
|
installState.value = FirmwareInstallState.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class FirmwareInstallState{
|
||||||
|
None,
|
||||||
|
Cancelled,
|
||||||
|
Verifying,
|
||||||
|
Query,
|
||||||
|
Install,
|
||||||
|
Done
|
||||||
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package org.ryujinx.android.viewmodels
|
package org.ryujinx.android.viewmodels
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.toLowerCase
|
import androidx.compose.ui.text.toLowerCase
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
|
import com.anggrayudi.storage.file.extension
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import org.ryujinx.android.Helpers
|
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@ -16,29 +19,32 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
private var basePath: String
|
private var basePath: String
|
||||||
private var updateJsonName = "updates.json"
|
private var updateJsonName = "updates.json"
|
||||||
private var storageHelper: SimpleStorageHelper
|
private var storageHelper: SimpleStorageHelper
|
||||||
|
var currentPaths: MutableList<String> = mutableListOf()
|
||||||
var pathsState: SnapshotStateList<String>? = null
|
var pathsState: SnapshotStateList<String>? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UpdateRequestCode = 1002
|
const val UpdateRequestCode = 1002
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Remove(index: Int) {
|
fun remove(index: Int) {
|
||||||
if (index <= 0)
|
if (index <= 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
data?.paths?.apply {
|
data?.paths?.apply {
|
||||||
val removed = removeAt(index - 1)
|
val str = removeAt(index - 1)
|
||||||
File(removed).deleteRecursively()
|
Uri.parse(str)?.apply {
|
||||||
|
storageHelper.storage.context.contentResolver.releasePersistableUriPermission(
|
||||||
|
this,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
pathsState?.clear()
|
pathsState?.clear()
|
||||||
pathsState?.addAll(this)
|
pathsState?.addAll(this)
|
||||||
|
currentPaths = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Add(
|
fun add() {
|
||||||
isCopying: MutableState<Boolean>,
|
|
||||||
copyProgress: MutableState<Float>,
|
|
||||||
currentProgressName: MutableState<String>
|
|
||||||
) {
|
|
||||||
val callBack = storageHelper.onFileSelected
|
val callBack = storageHelper.onFileSelected
|
||||||
|
|
||||||
storageHelper.onFileSelected = { requestCode, files ->
|
storageHelper.onFileSelected = { requestCode, files ->
|
||||||
@ -47,29 +53,29 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
if (requestCode == UpdateRequestCode) {
|
if (requestCode == UpdateRequestCode) {
|
||||||
val file = files.firstOrNull()
|
val file = files.firstOrNull()
|
||||||
file?.apply {
|
file?.apply {
|
||||||
// Copy updates to internal data folder
|
if(file.extension == "nsp"){
|
||||||
val updatePath = "$basePath/update"
|
storageHelper.storage.context.contentResolver.takePersistableUriPermission(file.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
File(updatePath).mkdirs()
|
currentPaths.add(file.uri.toString())
|
||||||
Helpers.copyToData(
|
|
||||||
this,
|
|
||||||
updatePath,
|
|
||||||
storageHelper,
|
|
||||||
isCopying,
|
|
||||||
copyProgress,
|
|
||||||
currentProgressName, ::refreshPaths
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPaths()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storageHelper.openFilePicker(UpdateRequestCode)
|
storageHelper.openFilePicker(UpdateRequestCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshPaths() {
|
private fun refreshPaths() {
|
||||||
data?.apply {
|
data?.apply {
|
||||||
val updatePath = "$basePath/update"
|
|
||||||
val existingPaths = mutableListOf<String>()
|
val existingPaths = mutableListOf<String>()
|
||||||
File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) }
|
currentPaths.forEach {
|
||||||
|
val uri = Uri.parse(it)
|
||||||
|
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||||
|
if(file?.exists() == true){
|
||||||
|
existingPaths.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!existingPaths.contains(selected)) {
|
if (!existingPaths.contains(selected)) {
|
||||||
selected = ""
|
selected = ""
|
||||||
@ -88,7 +94,6 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
openDialog: MutableState<Boolean>
|
openDialog: MutableState<Boolean>
|
||||||
) {
|
) {
|
||||||
data?.apply {
|
data?.apply {
|
||||||
val updatePath = "$basePath/update"
|
|
||||||
this.selected = ""
|
this.selected = ""
|
||||||
if (paths.isNotEmpty() && index > 0) {
|
if (paths.isNotEmpty() && index > 0) {
|
||||||
val ind = max(index - 1, paths.count() - 1)
|
val ind = max(index - 1, paths.count() - 1)
|
||||||
@ -98,18 +103,29 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
File(basePath).mkdirs()
|
File(basePath).mkdirs()
|
||||||
|
|
||||||
|
|
||||||
var metadata = TitleUpdateMetadata()
|
val metadata = TitleUpdateMetadata()
|
||||||
val savedUpdates = mutableListOf<String>()
|
val savedUpdates = mutableListOf<String>()
|
||||||
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
|
currentPaths.forEach {
|
||||||
|
val uri = Uri.parse(it)
|
||||||
|
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||||
|
if(file?.exists() == true){
|
||||||
|
savedUpdates.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
metadata.paths = savedUpdates
|
metadata.paths = savedUpdates
|
||||||
|
|
||||||
val selectedName = File(selected).name
|
if(selected.isNotEmpty()){
|
||||||
val newSelectedPath = "$updatePath/$selectedName"
|
val uri = Uri.parse(selected)
|
||||||
if (File(newSelectedPath).exists()) {
|
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||||
metadata.selected = newSelectedPath
|
if(file?.exists() == true){
|
||||||
|
metadata.selected = selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
metadata.selected = selected
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = gson.toJson(metadata)
|
val json = gson.toJson(metadata)
|
||||||
File("$basePath/$updateJsonName").writeText(json)
|
File("$basePath/$updateJsonName").writeText(json)
|
||||||
|
|
||||||
openDialog.value = false
|
openDialog.value = false
|
||||||
@ -137,10 +153,13 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
||||||
|
|
||||||
refreshPaths()
|
|
||||||
}
|
}
|
||||||
|
currentPaths = data?.paths ?: mutableListOf()
|
||||||
storageHelper = MainActivity.StorageHelper!!
|
storageHelper = MainActivity.StorageHelper!!
|
||||||
|
refreshPaths()
|
||||||
|
|
||||||
|
File("$basePath/update").deleteRecursively()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.wrapContentWidth
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@ -51,25 +50,13 @@ class DlcViews {
|
|||||||
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Column {
|
Column {
|
||||||
Row(modifier = Modifier.padding(8.dp)
|
Row(modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align(
|
Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align(
|
||||||
Alignment.CenterVertically
|
Alignment.CenterVertically
|
||||||
))
|
))
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.add(refresh)
|
|
||||||
},
|
|
||||||
modifier = Modifier.align(
|
|
||||||
Alignment.CenterVertically
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Add,
|
|
||||||
contentDescription = "Add"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -119,8 +106,18 @@ class DlcViews {
|
|||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(modifier = Modifier.align(Alignment.End)) {
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.padding(4.dp),
|
||||||
|
onClick = {
|
||||||
|
viewModel.add(refresh)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text("Add")
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.padding(4.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
openDialog.value = false
|
openDialog.value = false
|
||||||
viewModel.save(dlcList)
|
viewModel.save(dlcList)
|
||||||
@ -132,3 +129,4 @@ class DlcViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -20,6 +20,7 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -82,6 +83,9 @@ class GameViews {
|
|||||||
val enableVsync = remember {
|
val enableVsync = remember {
|
||||||
mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync)
|
mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync)
|
||||||
}
|
}
|
||||||
|
val enableMotion = remember {
|
||||||
|
mutableStateOf(QuickSettings(mainViewModel.activity).enableMotion)
|
||||||
|
}
|
||||||
val showMore = remember {
|
val showMore = remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@ -169,10 +173,36 @@ class GameViews {
|
|||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.padding(8.dp)) {
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Motion",
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
Switch(checked = enableMotion.value, onCheckedChange = {
|
||||||
|
showMore.value = false
|
||||||
|
enableMotion.value = !enableMotion.value
|
||||||
|
val settings = QuickSettings(mainViewModel.activity)
|
||||||
|
settings.enableMotion = enableMotion.value
|
||||||
|
settings.save()
|
||||||
|
if (enableMotion.value)
|
||||||
|
mainViewModel.motionSensorManager?.register()
|
||||||
|
else
|
||||||
|
mainViewModel.motionSensorManager?.unregister()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||||
showMore.value = false
|
showMore.value = false
|
||||||
showController.value = !showController.value
|
showController.value = !showController.value
|
||||||
|
ryujinxNative.inputReleaseTouchPoint()
|
||||||
mainViewModel.controller?.setVisible(showController.value)
|
mainViewModel.controller?.setVisible(showController.value)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -183,7 +213,9 @@ class GameViews {
|
|||||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||||
showMore.value = false
|
showMore.value = false
|
||||||
enableVsync.value = !enableVsync.value
|
enableVsync.value = !enableVsync.value
|
||||||
RyujinxNative.instance.graphicsRendererSetVsync(enableVsync.value)
|
RyujinxNative.instance.graphicsRendererSetVsync(
|
||||||
|
enableVsync.value
|
||||||
|
)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.vSync(),
|
imageVector = Icons.vSync(),
|
||||||
@ -196,6 +228,7 @@ class GameViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val showBackNotice = remember {
|
val showBackNotice = remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
@ -284,6 +317,8 @@ class GameViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainViewModel.activity.uiHandler.Compose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +336,7 @@ class GameViews {
|
|||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
color = MaterialTheme.colorScheme.surface.copy(0.4f)
|
color = MaterialTheme.colorScheme.background.copy(0.4f)
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
var gameTimeVal = 0.0
|
var gameTimeVal = 0.0
|
||||||
|
@ -4,6 +4,7 @@ import android.content.res.Resources
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -18,8 +19,12 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
@ -28,18 +33,15 @@ import androidx.compose.material.icons.filled.Search
|
|||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
import androidx.compose.material3.BottomAppBar
|
|
||||||
import androidx.compose.material3.BottomAppBarDefaults
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SearchBar
|
import androidx.compose.material3.SearchBar
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
@ -50,16 +52,22 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.anggrayudi.storage.extension.launchOnUiThread
|
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.GameModel
|
||||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||||
|
import org.ryujinx.android.viewmodels.QuickSettings
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
@ -67,7 +75,8 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
class HomeViews {
|
class HomeViews {
|
||||||
companion object {
|
companion object {
|
||||||
const val ImageSize = 150
|
const val ListImageSize = 150
|
||||||
|
const val GridImageSize = 300
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -75,11 +84,15 @@ class HomeViews {
|
|||||||
viewModel: HomeViewModel = HomeViewModel(),
|
viewModel: HomeViewModel = HomeViewModel(),
|
||||||
navController: NavHostController? = null
|
navController: NavHostController? = null
|
||||||
) {
|
) {
|
||||||
|
viewModel.ensureReloadIfNecessary()
|
||||||
val showAppActions = remember { mutableStateOf(false) }
|
val showAppActions = remember { mutableStateOf(false) }
|
||||||
val showLoading = remember { mutableStateOf(false) }
|
val showLoading = remember { mutableStateOf(false) }
|
||||||
val openTitleUpdateDialog = remember { mutableStateOf(false) }
|
val openTitleUpdateDialog = remember { mutableStateOf(false) }
|
||||||
val canClose = remember { mutableStateOf(true) }
|
val canClose = remember { mutableStateOf(true) }
|
||||||
val openDlcDialog = remember { mutableStateOf(false) }
|
val openDlcDialog = remember { mutableStateOf(false) }
|
||||||
|
val selectedModel = remember {
|
||||||
|
mutableStateOf(viewModel.mainViewModel?.selected)
|
||||||
|
}
|
||||||
val query = remember {
|
val query = remember {
|
||||||
mutableStateOf("")
|
mutableStateOf("")
|
||||||
}
|
}
|
||||||
@ -153,10 +166,134 @@ class HomeViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
bottomBar = {
|
|
||||||
BottomAppBar(
|
) { contentPadding ->
|
||||||
actions = {
|
Box(modifier = Modifier.padding(contentPadding)) {
|
||||||
|
val list = remember {
|
||||||
|
viewModel.gameList
|
||||||
|
}
|
||||||
|
viewModel.filter(query.value)
|
||||||
|
var settings = QuickSettings(viewModel.activity!!)
|
||||||
|
|
||||||
|
if (settings.isGrid) {
|
||||||
|
val size = GridImageSize / Resources.getSystem().displayMetrics.density
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = (size + 4).dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
items(list) {
|
||||||
|
it.titleName?.apply {
|
||||||
|
if (this.isNotEmpty() && (query.value.trim()
|
||||||
|
.isEmpty() || this.lowercase(Locale.getDefault())
|
||||||
|
.contains(query.value))
|
||||||
|
)
|
||||||
|
GridGameItem(
|
||||||
|
it,
|
||||||
|
viewModel,
|
||||||
|
showAppActions,
|
||||||
|
showLoading,
|
||||||
|
selectedModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
|
items(list) {
|
||||||
|
it.titleName?.apply {
|
||||||
|
if (this.isNotEmpty() && (query.value.trim()
|
||||||
|
.isEmpty() || this.lowercase(
|
||||||
|
Locale.getDefault()
|
||||||
|
)
|
||||||
|
.contains(query.value))
|
||||||
|
)
|
||||||
|
ListGameItem(
|
||||||
|
it,
|
||||||
|
viewModel,
|
||||||
|
showAppActions,
|
||||||
|
showLoading,
|
||||||
|
selectedModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLoading.value) {
|
||||||
|
AlertDialog(onDismissRequest = { }) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = "Loading")
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openTitleUpdateDialog.value) {
|
||||||
|
AlertDialog(onDismissRequest = {
|
||||||
|
openTitleUpdateDialog.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 ?: ""
|
||||||
|
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAppActions.value)
|
||||||
|
ModalBottomSheet(
|
||||||
|
content = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
if (showAppActions.value) {
|
if (showAppActions.value) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (viewModel.mainViewModel?.selected != null) {
|
if (viewModel.mainViewModel?.selected != null) {
|
||||||
@ -225,154 +362,18 @@ class HomeViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*\val showAppletMenu = remember { mutableStateOf(false) }
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = {
|
|
||||||
showAppletMenu.value = true
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
org.ryujinx.android.Icons.applets(MaterialTheme.colorScheme.onSurface),
|
|
||||||
contentDescription = "Applets"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
|
||||||
expanded = showAppletMenu.value,
|
|
||||||
onDismissRequest = { showAppletMenu.value = false }) {
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(text = "Launch Mii Editor")
|
|
||||||
}, onClick = {
|
|
||||||
showAppletMenu.value = false
|
|
||||||
showLoading.value = true
|
|
||||||
thread {
|
|
||||||
val success =
|
|
||||||
viewModel.mainViewModel?.loadMiiEditor() ?: false
|
|
||||||
if (success) {
|
|
||||||
launchOnUiThread {
|
|
||||||
viewModel.mainViewModel?.navigateToGame()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
viewModel.mainViewModel!!.isMiiEditorLaunched = false
|
|
||||||
showLoading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
onDismissRequest = {
|
||||||
FloatingActionButton(
|
showAppActions.value = false
|
||||||
onClick = {
|
selectedModel.value = null
|
||||||
viewModel.openGameFolder()
|
|
||||||
},
|
|
||||||
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
|
|
||||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface),
|
|
||||||
contentDescription = "Open Folder"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
) { contentPadding ->
|
|
||||||
Box(modifier = Modifier.padding(contentPadding)) {
|
|
||||||
val list = remember {
|
|
||||||
viewModel.gameList
|
|
||||||
}
|
|
||||||
val selectedModel = remember {
|
|
||||||
mutableStateOf(viewModel.mainViewModel?.selected)
|
|
||||||
}
|
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
|
||||||
items(list) {
|
|
||||||
it.titleName?.apply {
|
|
||||||
if (this.isNotEmpty() && (query.value.trim()
|
|
||||||
.isEmpty() || this.lowercase(
|
|
||||||
Locale.getDefault()
|
|
||||||
)
|
|
||||||
.contains(query.value))
|
|
||||||
)
|
|
||||||
GameItem(
|
|
||||||
it,
|
|
||||||
viewModel,
|
|
||||||
showAppActions,
|
|
||||||
showLoading,
|
|
||||||
selectedModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showLoading.value) {
|
|
||||||
AlertDialog(onDismissRequest = { }) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(text = "Loading")
|
|
||||||
LinearProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openTitleUpdateDialog.value) {
|
|
||||||
AlertDialog(onDismissRequest = {
|
|
||||||
openTitleUpdateDialog.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 ?: ""
|
|
||||||
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GameItem(
|
fun ListGameItem(
|
||||||
gameModel: GameModel,
|
gameModel: GameModel,
|
||||||
viewModel: HomeViewModel,
|
viewModel: HomeViewModel,
|
||||||
showAppActions: MutableState<Boolean>,
|
showAppActions: MutableState<Boolean>,
|
||||||
@ -400,7 +401,7 @@ class HomeViews {
|
|||||||
selected = null
|
selected = null
|
||||||
}
|
}
|
||||||
selectedModel.value = 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 {
|
thread {
|
||||||
showLoading.value = true
|
showLoading.value = true
|
||||||
val success =
|
val success =
|
||||||
@ -429,10 +430,11 @@ class HomeViews {
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
|
||||||
if (gameModel.icon?.isNotEmpty() == true) {
|
if (gameModel.icon?.isNotEmpty() == true) {
|
||||||
val pic = decoder.decode(gameModel.icon)
|
val pic = decoder.decode(gameModel.icon)
|
||||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
val size =
|
||||||
|
ListImageSize / Resources.getSystem().displayMetrics.density
|
||||||
Image(
|
Image(
|
||||||
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
|
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
|
||||||
.asImageBitmap(),
|
.asImageBitmap(),
|
||||||
@ -442,7 +444,9 @@ class HomeViews {
|
|||||||
.width(size.roundToInt().dp)
|
.width(size.roundToInt().dp)
|
||||||
.height(size.roundToInt().dp)
|
.height(size.roundToInt().dp)
|
||||||
)
|
)
|
||||||
} else NotAvailableIcon()
|
} else if (gameModel.type == FileType.Nro)
|
||||||
|
NROIcon()
|
||||||
|
else NotAvailableIcon()
|
||||||
} else NotAvailableIcon()
|
} else NotAvailableIcon()
|
||||||
Column {
|
Column {
|
||||||
Text(text = gameModel.titleName ?: "")
|
Text(text = gameModel.titleName ?: "")
|
||||||
@ -458,12 +462,107 @@ class HomeViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun GridGameItem(
|
||||||
|
gameModel: GameModel,
|
||||||
|
viewModel: HomeViewModel,
|
||||||
|
showAppActions: MutableState<Boolean>,
|
||||||
|
showLoading: MutableState<Boolean>,
|
||||||
|
selectedModel: MutableState<GameModel?>
|
||||||
|
) {
|
||||||
|
remember {
|
||||||
|
selectedModel
|
||||||
|
}
|
||||||
|
val color =
|
||||||
|
if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
|
||||||
|
|
||||||
|
val decoder = Base64.getDecoder()
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = color,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.mainViewModel?.selected != null) {
|
||||||
|
showAppActions.value = false
|
||||||
|
viewModel.mainViewModel?.apply {
|
||||||
|
selected = null
|
||||||
|
}
|
||||||
|
selectedModel.value = null
|
||||||
|
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
|
||||||
|
thread {
|
||||||
|
showLoading.value = true
|
||||||
|
val success =
|
||||||
|
viewModel.mainViewModel?.loadGame(gameModel) ?: false
|
||||||
|
if (success) {
|
||||||
|
launchOnUiThread {
|
||||||
|
viewModel.mainViewModel?.navigateToGame()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gameModel.close()
|
||||||
|
}
|
||||||
|
showLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
viewModel.mainViewModel?.selected = gameModel
|
||||||
|
showAppActions.value = true
|
||||||
|
selectedModel.value = gameModel
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(4.dp)) {
|
||||||
|
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
|
||||||
|
Image(
|
||||||
|
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
|
||||||
|
.asImageBitmap(),
|
||||||
|
contentDescription = gameModel.titleName + " icon",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(0.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
} 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)
|
||||||
|
.basicMarquee()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotAvailableIcon() {
|
fun NotAvailableIcon() {
|
||||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
val size = ListImageSize / Resources.getSystem().displayMetrics.density
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Add,
|
Icons.Filled.Add,
|
||||||
contentDescription = "Options",
|
contentDescription = "N/A",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.width(size.roundToInt().dp)
|
||||||
|
.height(size.roundToInt().dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NROIcon() {
|
||||||
|
val size = ListImageSize / Resources.getSystem().displayMetrics.density
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.icon_nro),
|
||||||
|
contentDescription = "NRO",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 8.dp)
|
.padding(end = 8.dp)
|
||||||
.width(size.roundToInt().dp)
|
.width(size.roundToInt().dp)
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package org.ryujinx.android.views
|
package org.ryujinx.android.views
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
@ -14,6 +17,8 @@ import androidx.compose.animation.shrinkVertically
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@ -56,6 +61,8 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import com.anggrayudi.storage.file.extension
|
import com.anggrayudi.storage.file.extension
|
||||||
import org.ryujinx.android.Helpers
|
import org.ryujinx.android.Helpers
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import org.ryujinx.android.providers.DocumentProvider
|
||||||
|
import org.ryujinx.android.viewmodels.FirmwareInstallState
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
import org.ryujinx.android.viewmodels.SettingsViewModel
|
import org.ryujinx.android.viewmodels.SettingsViewModel
|
||||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||||
@ -66,7 +73,7 @@ class SettingViews {
|
|||||||
const val EXPANSTION_TRANSITION_DURATION = 450
|
const val EXPANSTION_TRANSITION_DURATION = 450
|
||||||
const val IMPORT_CODE = 12341
|
const val IMPORT_CODE = 12341
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) {
|
fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) {
|
||||||
val loaded = remember {
|
val loaded = remember {
|
||||||
@ -103,6 +110,27 @@ class SettingViews {
|
|||||||
val useVirtualController = remember {
|
val useVirtualController = remember {
|
||||||
mutableStateOf(true)
|
mutableStateOf(true)
|
||||||
}
|
}
|
||||||
|
val showFirwmareDialog = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
val firmwareInstallState = remember {
|
||||||
|
mutableStateOf(FirmwareInstallState.None)
|
||||||
|
}
|
||||||
|
val firmwareVersion = remember {
|
||||||
|
mutableStateOf(mainViewModel.firmwareVersion)
|
||||||
|
}
|
||||||
|
val isGrid = remember { mutableStateOf(true) }
|
||||||
|
val useSwitchLayout = remember { mutableStateOf(true) }
|
||||||
|
val enableMotion = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
val enableDebugLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableStubLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableInfoLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableWarningLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableErrorLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableGuestLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableAccessLogs = remember { mutableStateOf(true) }
|
||||||
|
val enableTraceLogs = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
if (!loaded.value) {
|
if (!loaded.value) {
|
||||||
settingsViewModel.initializeState(
|
settingsViewModel.initializeState(
|
||||||
@ -112,7 +140,18 @@ class SettingViews {
|
|||||||
enableShaderCache,
|
enableShaderCache,
|
||||||
enableTextureRecompression,
|
enableTextureRecompression,
|
||||||
resScale,
|
resScale,
|
||||||
useVirtualController
|
useVirtualController,
|
||||||
|
isGrid,
|
||||||
|
useSwitchLayout,
|
||||||
|
enableMotion,
|
||||||
|
enableDebugLogs,
|
||||||
|
enableStubLogs,
|
||||||
|
enableInfoLogs,
|
||||||
|
enableWarningLogs,
|
||||||
|
enableErrorLogs,
|
||||||
|
enableGuestLogs,
|
||||||
|
enableAccessLogs,
|
||||||
|
enableTraceLogs
|
||||||
)
|
)
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
}
|
}
|
||||||
@ -134,7 +173,18 @@ class SettingViews {
|
|||||||
enableShaderCache,
|
enableShaderCache,
|
||||||
enableTextureRecompression,
|
enableTextureRecompression,
|
||||||
resScale,
|
resScale,
|
||||||
useVirtualController
|
useVirtualController,
|
||||||
|
isGrid,
|
||||||
|
useSwitchLayout,
|
||||||
|
enableMotion,
|
||||||
|
enableDebugLogs,
|
||||||
|
enableStubLogs,
|
||||||
|
enableInfoLogs,
|
||||||
|
enableWarningLogs,
|
||||||
|
enableErrorLogs,
|
||||||
|
enableGuestLogs,
|
||||||
|
enableAccessLogs,
|
||||||
|
enableTraceLogs
|
||||||
)
|
)
|
||||||
settingsViewModel.navController.popBackStack()
|
settingsViewModel.navController.popBackStack()
|
||||||
}) {
|
}) {
|
||||||
@ -145,6 +195,223 @@ class SettingViews {
|
|||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.verticalScroll(rememberScrollState())) {
|
.verticalScroll(rememberScrollState())) {
|
||||||
|
ExpandableView(onCardArrowClick = { }, title = "App") {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Use Grid",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = isGrid.value, onCheckedChange = {
|
||||||
|
isGrid.value = !isGrid.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Game Folder",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Button(onClick = {
|
||||||
|
settingsViewModel.openGameFolder()
|
||||||
|
}) {
|
||||||
|
Text(text = "Choose Folder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "System Firmware",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = firmwareVersion.value,
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
fun createIntent(action: String): Intent {
|
||||||
|
val intent = Intent(action)
|
||||||
|
intent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
intent.data = DocumentsContract.buildRootUri(
|
||||||
|
DocumentProvider.AUTHORITY,
|
||||||
|
DocumentProvider.ROOT_ID
|
||||||
|
)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE"))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui"))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent("com.android.documentsui"))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = "Open App Folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
settingsViewModel.importProdKeys()
|
||||||
|
}) {
|
||||||
|
Text(text = "Import prod Keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
showFirwmareDialog.value = true
|
||||||
|
}) {
|
||||||
|
Text(text = "Install Firmware")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showFirwmareDialog.value) {
|
||||||
|
AlertDialog(onDismissRequest = {
|
||||||
|
if(firmwareInstallState.value != FirmwareInstallState.Install) {
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(firmwareInstallState)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
if (firmwareInstallState.value == FirmwareInstallState.None) {
|
||||||
|
Text(text = "Select a zip or XCI file to install from.")
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
settingsViewModel.selectFirmware(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "Select File")
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (firmwareInstallState.value == FirmwareInstallState.Query) {
|
||||||
|
Text(text = "Firmware ${settingsViewModel.selectedFirmwareVersion} will be installed. Do you want to continue?")
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
settingsViewModel.installFirmware(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
|
||||||
|
if(firmwareInstallState.value == FirmwareInstallState.None){
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(firmwareInstallState)
|
||||||
|
}
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "Yes")
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "No")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (firmwareInstallState.value == FirmwareInstallState.Install) {
|
||||||
|
Text(text = "Installing Firmware ${settingsViewModel.selectedFirmwareVersion}...")
|
||||||
|
LinearProgressIndicator(modifier = Modifier
|
||||||
|
.padding(top = 4.dp))
|
||||||
|
} else if (firmwareInstallState.value == FirmwareInstallState.Verifying) {
|
||||||
|
Text(text = "Verifying selected file...")
|
||||||
|
LinearProgressIndicator(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else if (firmwareInstallState.value == FirmwareInstallState.Done) {
|
||||||
|
Text(text = "Installed Firmware ${settingsViewModel.selectedFirmwareVersion}")
|
||||||
|
firmwareVersion.value = mainViewModel.firmwareVersion
|
||||||
|
}
|
||||||
|
else if(firmwareInstallState.value == FirmwareInstallState.Cancelled){
|
||||||
|
val file = settingsViewModel.selectedFirmwareFile
|
||||||
|
if(file != null){
|
||||||
|
if(file.extension == "xci" || file.extension == "zip"){
|
||||||
|
if(settingsViewModel.selectedFirmwareVersion.isEmpty()) {
|
||||||
|
Text(text = "Unable to find version in selected file")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(text = "Unknown Error has occurred. Please check logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(text = "File type is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(text = "File type is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ExpandableView(onCardArrowClick = { }, title = "System") {
|
ExpandableView(onCardArrowClick = { }, title = "System") {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
@ -330,15 +597,17 @@ class SettingViews {
|
|||||||
showImportCompletion.value = false
|
showImportCompletion.value = false
|
||||||
importFile.value = null
|
importFile.value = null
|
||||||
mainViewModel.userViewModel.refreshUsers()
|
mainViewModel.userViewModel.refreshUsers()
|
||||||
mainViewModel.homeViewModel.clearLoadedCache()
|
mainViewModel.homeViewModel.requestReload()
|
||||||
}) {
|
}) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
) {
|
) {
|
||||||
Text(modifier = Modifier
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
text = "App Data import completed.")
|
text = "App Data import completed."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -591,6 +860,173 @@ class SettingViews {
|
|||||||
useVirtualController.value = !useVirtualController.value
|
useVirtualController.value = !useVirtualController.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Motion",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableMotion.value, onCheckedChange = {
|
||||||
|
enableMotion.value = !enableMotion.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Use Switch Controller Layout",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = useSwitchLayout.value, onCheckedChange = {
|
||||||
|
useSwitchLayout.value = !useSwitchLayout.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpandableView(onCardArrowClick = { }, title = "Log") {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Debug Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableDebugLogs.value, onCheckedChange = {
|
||||||
|
enableDebugLogs.value = !enableDebugLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Stub Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableStubLogs.value, onCheckedChange = {
|
||||||
|
enableStubLogs.value = !enableStubLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Info Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableInfoLogs.value, onCheckedChange = {
|
||||||
|
enableInfoLogs.value = !enableInfoLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Warning Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableWarningLogs.value, onCheckedChange = {
|
||||||
|
enableWarningLogs.value = !enableWarningLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Error Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableErrorLogs.value, onCheckedChange = {
|
||||||
|
enableErrorLogs.value = !enableErrorLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Guest Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableGuestLogs.value, onCheckedChange = {
|
||||||
|
enableGuestLogs.value = !enableGuestLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Access Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableAccessLogs.value, onCheckedChange = {
|
||||||
|
enableAccessLogs.value = !enableAccessLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enable Trace Logs",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Switch(checked = enableTraceLogs.value, onCheckedChange = {
|
||||||
|
enableTraceLogs.value = !enableTraceLogs.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
mainViewModel.logging.requestExport()
|
||||||
|
}) {
|
||||||
|
Text(text = "Send Logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -602,7 +1038,18 @@ class SettingViews {
|
|||||||
enableShaderCache,
|
enableShaderCache,
|
||||||
enableTextureRecompression,
|
enableTextureRecompression,
|
||||||
resScale,
|
resScale,
|
||||||
useVirtualController
|
useVirtualController,
|
||||||
|
isGrid,
|
||||||
|
useSwitchLayout,
|
||||||
|
enableMotion,
|
||||||
|
enableDebugLogs,
|
||||||
|
enableStubLogs,
|
||||||
|
enableInfoLogs,
|
||||||
|
enableWarningLogs,
|
||||||
|
enableErrorLogs,
|
||||||
|
enableGuestLogs,
|
||||||
|
enableAccessLogs,
|
||||||
|
enableTraceLogs
|
||||||
)
|
)
|
||||||
settingsViewModel.navController.popBackStack()
|
settingsViewModel.navController.popBackStack()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.ryujinx.android.views
|
package org.ryujinx.android.views
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -13,7 +14,6 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@ -28,13 +28,19 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.ryujinx.android.MainActivity
|
||||||
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class TitleUpdateViews {
|
class TitleUpdateViews {
|
||||||
companion object {
|
companion object {
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
|
fun Main(
|
||||||
|
titleId: String,
|
||||||
|
name: String,
|
||||||
|
openDialog: MutableState<Boolean>,
|
||||||
|
canClose: MutableState<Boolean>
|
||||||
|
) {
|
||||||
val viewModel = TitleUpdateViewModel(titleId)
|
val viewModel = TitleUpdateViewModel(titleId)
|
||||||
|
|
||||||
val selected = remember { mutableStateOf(0) }
|
val selected = remember { mutableStateOf(0) }
|
||||||
@ -43,15 +49,6 @@ class TitleUpdateViews {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
val isCopying = remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
val copyProgress = remember {
|
|
||||||
mutableStateOf(0.0f)
|
|
||||||
}
|
|
||||||
var currentProgressName = remember {
|
|
||||||
mutableStateOf("Starting Copy")
|
|
||||||
}
|
|
||||||
Column {
|
Column {
|
||||||
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
||||||
Surface(
|
Surface(
|
||||||
@ -88,18 +85,24 @@ class TitleUpdateViews {
|
|||||||
var index = 1
|
var index = 1
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
val i = index
|
val i = index
|
||||||
|
val uri = Uri.parse(path)
|
||||||
|
val file = DocumentFile.fromSingleUri(
|
||||||
|
MainActivity.mainViewModel!!.activity,
|
||||||
|
uri
|
||||||
|
)
|
||||||
|
file?.apply {
|
||||||
Row(modifier = Modifier.padding(8.dp)) {
|
Row(modifier = Modifier.padding(8.dp)) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = (selected.value == i),
|
selected = (selected.value == i),
|
||||||
onClick = { selected.value = i })
|
onClick = { selected.value = i })
|
||||||
Text(
|
Text(
|
||||||
text = File(path).name,
|
text = file.name ?: "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +110,7 @@ class TitleUpdateViews {
|
|||||||
Row(modifier = Modifier.align(Alignment.End)) {
|
Row(modifier = Modifier.align(Alignment.End)) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.Remove(selected.value)
|
viewModel.remove(selected.value)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -118,7 +121,7 @@ class TitleUpdateViews {
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.Add(isCopying, copyProgress, currentProgressName)
|
viewModel.add()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -129,33 +132,12 @@ class TitleUpdateViews {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if (isCopying.value) {
|
|
||||||
Text(text = "Copying updates to local storage")
|
|
||||||
Text(text = currentProgressName.value)
|
|
||||||
Row {
|
|
||||||
LinearProgressIndicator(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
progress = copyProgress.value
|
|
||||||
)
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
isCopying.value = false
|
|
||||||
canClose.value = true
|
|
||||||
viewModel.refreshPaths()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isCopying.value) {
|
|
||||||
canClose.value = true
|
canClose.value = true
|
||||||
viewModel.save(selected.value, openDialog)
|
viewModel.save(selected.value, openDialog)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
|
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 |
12
src/RyujinxAndroid/app/src/main/res/xml/provider_paths.xml
Normal file
12
src/RyujinxAndroid/app/src/main/res/xml/provider_paths.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path
|
||||||
|
name="external"
|
||||||
|
path="/" />
|
||||||
|
<external-files-path
|
||||||
|
name="external_files"
|
||||||
|
path="/" />
|
||||||
|
<files-path
|
||||||
|
name="files"
|
||||||
|
path="/" />
|
||||||
|
</paths>
|
@ -24,7 +24,7 @@ android.nonTransitiveRClass=true
|
|||||||
# Build configuration
|
# Build configuration
|
||||||
# It needs to be set to either "debug" or "release" and can also be overriden on a per build basis
|
# It needs to be set to either "debug" or "release" and can also be overriden on a per build basis
|
||||||
# by adding -Dorg.ryujinx.config=NAME to the command line.
|
# by adding -Dorg.ryujinx.config=NAME to the command line.
|
||||||
org.ryujinx.config=debug
|
org.ryujinx.config=release
|
||||||
# Controls stripping of symbols from libryujinx
|
# Controls stripping of symbols from libryujinx
|
||||||
# Setting this property to auto causes symbols to be stripped for release builds,
|
# Setting this property to auto causes symbols to be stripped for release builds,
|
||||||
# but not for debug builds.
|
# but not for debug builds.
|
||||||
@ -33,3 +33,4 @@ org.ryujinx.config=debug
|
|||||||
org.ryujinx.symbols.strip=auto
|
org.ryujinx.symbols.strip=auto
|
||||||
# Output path of libryujinx.so
|
# Output path of libryujinx.so
|
||||||
org.ryujinx.publish.path=app/src/main/jniLibs/arm64-v8a
|
org.ryujinx.publish.path=app/src/main/jniLibs/arm64-v8a
|
||||||
|
org.ryujinx.llvm.toolchain.path=C\:\\Android\\android-sdk\\ndk\\25.1.8937393\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin
|
||||||
|
Loading…
x
Reference in New Issue
Block a user