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.Targets;
|
||||
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.Primitives;
|
||||
using LibRyujinx.Jni.References;
|
||||
@ -42,11 +42,36 @@ namespace LibRyujinx
|
||||
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
||||
|
||||
[DllImport("libryujinxjni")]
|
||||
private extern static long storeString(string ch);
|
||||
[DllImport("libryujinxjni")]
|
||||
private extern static IntPtr getString(long id);
|
||||
internal extern static long storeString(string ch);
|
||||
|
||||
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);
|
||||
if (pointer != IntPtr.Zero)
|
||||
@ -84,21 +109,21 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
[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");
|
||||
PlatformInfo.IsBionic = true;
|
||||
|
||||
Logger.AddTarget(
|
||||
new AsyncLogTargetWrapper(
|
||||
new AndroidLogTarget("Ryujinx"),
|
||||
new AndroidLogTarget("RyujinxLog"),
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
|
||||
var path = GetStoredString(jpathId);
|
||||
|
||||
var init = Initialize(path, enableDebugLogs);
|
||||
var init = Initialize(path);
|
||||
|
||||
_surfaceEvent?.Set();
|
||||
|
||||
@ -237,7 +262,7 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type, JInt updateDescriptor)
|
||||
{
|
||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||
if (SwitchDevice?.EmulationContext == null)
|
||||
@ -246,8 +271,62 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
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")]
|
||||
@ -413,6 +492,13 @@ namespace LibRyujinx
|
||||
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")]
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path)
|
||||
{
|
||||
@ -422,12 +508,12 @@ namespace LibRyujinx
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci)
|
||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JLong extension)
|
||||
{
|
||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||
using var stream = OpenFile(fileDescriptor);
|
||||
|
||||
var info = GetGameInfo(stream, isXci);
|
||||
var ext = GetStoredString(extension);
|
||||
var info = GetGameInfo(stream, ext.ToLower());
|
||||
return GetInfo(jEnv, info);
|
||||
}
|
||||
|
||||
@ -546,6 +632,20 @@ namespace LibRyujinx
|
||||
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")]
|
||||
public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id)
|
||||
{
|
||||
@ -635,6 +735,30 @@ namespace LibRyujinx
|
||||
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")]
|
||||
public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr)
|
||||
{
|
||||
|
@ -11,8 +11,8 @@ android {
|
||||
applicationId "org.ryujinx.android"
|
||||
minSdk 30
|
||||
targetSdk 33
|
||||
versionCode 10004
|
||||
versionName '1.0.4'
|
||||
versionCode 10010
|
||||
versionName '1.0.10'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@ -49,6 +49,7 @@ android {
|
||||
buildFeatures {
|
||||
compose true
|
||||
prefab true
|
||||
buildConfig true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.3.2'
|
||||
|
@ -17,6 +17,7 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".RyujinxApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="game"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@ -39,6 +40,26 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
@ -38,6 +38,36 @@
|
||||
|
||||
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
|
||||
bool (*initialize)(char*) = NULL;
|
||||
|
||||
@ -46,7 +76,7 @@ long _currentRenderingThreadId = 0;
|
||||
JavaVM* _vm = nullptr;
|
||||
jobject _mainActivity = nullptr;
|
||||
jclass _mainActivityClass = nullptr;
|
||||
std::string _currentString = "";
|
||||
string_helper str_helper = string_helper();
|
||||
UiHandler ui_handler = UiHandler();
|
||||
|
||||
#endif //RYUJINXNATIVE_RYUIJNX_H
|
||||
|
@ -185,6 +185,8 @@ void setProgressInfo(char* info, float progressValue) {
|
||||
progress = progressValue;
|
||||
}
|
||||
|
||||
bool isInitialOrientationFlipped = true;
|
||||
|
||||
extern "C"
|
||||
void setCurrentTransform(long native_window, int transform){
|
||||
if(native_window == 0 || native_window == -1)
|
||||
@ -204,7 +206,7 @@ void setCurrentTransform(long native_window, int transform){
|
||||
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90;
|
||||
break;
|
||||
case 0x4:
|
||||
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180;
|
||||
nativeTransform = isInitialOrientationFlipped ? ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY : ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180;
|
||||
break;
|
||||
case 0x8:
|
||||
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270;
|
||||
@ -325,6 +327,37 @@ const char* getString(long id){
|
||||
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"
|
||||
JNIEXPORT jlong JNICALL
|
||||
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"
|
||||
JNIEXPORT jstring JNICALL
|
||||
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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
abstract class BaseActivity : ComponentActivity() {
|
||||
companion object{
|
||||
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) {
|
||||
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.viewinterop.AndroidView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.swordfish.radialgamepad.library.RadialGamePad
|
||||
import com.swordfish.radialgamepad.library.config.ButtonConfig
|
||||
@ -29,6 +27,7 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
|
||||
typealias GamePad = RadialGamePad
|
||||
typealias GamePadConfig = RadialGamePadConfig
|
||||
@ -65,6 +64,7 @@ class GameController(var activity: Activity) {
|
||||
}
|
||||
controller.controllerView = c
|
||||
viewModel.setGameController(controller)
|
||||
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
|
||||
c
|
||||
})
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import android.view.SurfaceView
|
||||
import androidx.compose.runtime.MutableState
|
||||
import org.ryujinx.android.viewmodels.GameModel
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
@ -76,6 +75,8 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||
_isInit = false
|
||||
_isStarted = false
|
||||
|
||||
mainViewModel.activity.uiHandler.stop()
|
||||
|
||||
_updateThread?.join()
|
||||
_renderingThreadWatcher?.join()
|
||||
}
|
||||
@ -88,21 +89,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||
|
||||
_nativeRyujinx.inputInitialize(width, height)
|
||||
|
||||
val settings = QuickSettings(mainViewModel.activity)
|
||||
|
||||
if (!settings.useVirtualController) {
|
||||
mainViewModel.controller?.setVisible(false)
|
||||
} else {
|
||||
mainViewModel.controller?.connect()
|
||||
}
|
||||
|
||||
mainViewModel.physicalControllerManager?.connect()
|
||||
val id = mainViewModel.physicalControllerManager?.connect()
|
||||
mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
|
||||
|
||||
_nativeRyujinx.graphicsRendererSetSize(
|
||||
surfaceHolder.surfaceFrame.width(),
|
||||
surfaceHolder.surfaceFrame.height()
|
||||
)
|
||||
|
||||
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
|
||||
|
||||
_guestThread = thread(start = true) {
|
||||
runGame()
|
||||
}
|
||||
@ -167,6 +163,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||
mainViewModel.performanceManager?.closeCurrentRenderingSession()
|
||||
}
|
||||
}
|
||||
|
||||
thread {
|
||||
mainViewModel.activity.uiHandler.listen()
|
||||
}
|
||||
_nativeRyujinx.graphicsRendererRunLoop()
|
||||
|
||||
game?.close()
|
||||
|
@ -72,6 +72,7 @@ class Helpers {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun copyToData(
|
||||
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
|
||||
isCopying: MutableState<Boolean>,
|
||||
@ -79,17 +80,18 @@ class Helpers {
|
||||
currentProgressName: MutableState<String>,
|
||||
finish: () -> Unit
|
||||
) {
|
||||
var fPath = path + "/${file.name}";
|
||||
var callback: FileCallback? = object : FileCallback() {
|
||||
override fun onFailed(errorCode: FileCallback.ErrorCode) {
|
||||
super.onFailed(errorCode)
|
||||
File(path).delete()
|
||||
File(fPath).delete()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onStart(file: Any, workerThread: Thread): Long {
|
||||
copyProgress.value = 0f
|
||||
|
||||
(file as DocumentFile)?.apply {
|
||||
(file as DocumentFile).apply {
|
||||
currentProgressName.value = "Copying ${file.name}"
|
||||
}
|
||||
return super.onStart(file, workerThread)
|
||||
@ -113,8 +115,8 @@ class Helpers {
|
||||
}
|
||||
val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
isCopying.value = true
|
||||
File(fPath).delete()
|
||||
file.apply {
|
||||
if (!File(path + "/${file.name}").exists()) {
|
||||
val f = this
|
||||
ioScope.launch {
|
||||
f.copyFileTo(
|
||||
@ -126,7 +128,6 @@ class Helpers {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDataColumn(
|
||||
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.WindowInsetsControllerCompat
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.halilibo.richtext.ui.RichTextThemeIntegration
|
||||
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import org.ryujinx.android.views.MainView
|
||||
import kotlin.math.abs
|
||||
|
||||
@ -27,9 +29,11 @@ import kotlin.math.abs
|
||||
class MainActivity : BaseActivity() {
|
||||
private var physicalControllerManager: PhysicalControllerManager =
|
||||
PhysicalControllerManager(this)
|
||||
private lateinit var motionSensorManager: MotionSensorManager
|
||||
private var _isInit: Boolean = false
|
||||
var isGameRunning = false
|
||||
var storageHelper: SimpleStorageHelper? = null
|
||||
lateinit var uiHandler: UiHandler
|
||||
companion object {
|
||||
var mainViewModel: MainViewModel? = null
|
||||
var AppPath : String = ""
|
||||
@ -64,12 +68,27 @@ class MainActivity : BaseActivity() {
|
||||
return
|
||||
|
||||
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
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
motionSensorManager = MotionSensorManager(this)
|
||||
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
||||
|
||||
if(
|
||||
!Environment.isExternalStorageManager()
|
||||
) {
|
||||
@ -78,7 +97,6 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
||||
|
||||
|
||||
initialize()
|
||||
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
@ -86,10 +104,14 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
mainViewModel = MainViewModel(this)
|
||||
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||
mainViewModel!!.motionSensorManager = motionSensorManager
|
||||
|
||||
mainViewModel!!.refreshFirmwareVersion()
|
||||
|
||||
mainViewModel?.apply {
|
||||
setContent {
|
||||
RyujinxAndroidTheme {
|
||||
RichTextThemeIntegration(contentColor = { MaterialTheme.colorScheme.onSurface }) {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@ -101,6 +123,7 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
storageHelper?.onSaveInstanceState(outState)
|
||||
@ -133,7 +156,7 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
fun setFullScreen(fullscreen: Boolean) {
|
||||
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)
|
||||
|
||||
@ -183,6 +206,8 @@ class MainActivity : BaseActivity() {
|
||||
setFullScreen(true)
|
||||
NativeHelpers.instance.setTurboMode(true)
|
||||
force60HzRefreshRate(true)
|
||||
if (QuickSettings(this).enableMotion)
|
||||
motionSensorManager.register()
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,5 +218,7 @@ class MainActivity : BaseActivity() {
|
||||
NativeHelpers.instance.setTurboMode(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 storeStringJava(string: String) : Long
|
||||
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
|
||||
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
|
||||
class PhysicalControllerManager(val activity: MainActivity) {
|
||||
private var controllerId: Int = -1
|
||||
private var ryujinxNative: RyujinxNative = RyujinxNative.instance
|
||||
|
||||
fun onKeyEvent(event: KeyEvent) : Boolean{
|
||||
if(controllerId != -1) {
|
||||
val id = getGamePadButtonInputId(event.keyCode)
|
||||
|
||||
if(id != GamePadButtonInputId.None) {
|
||||
val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0
|
||||
if (/*controllerId != -1 &&*/ isNotFallback) {
|
||||
when (event.action) {
|
||||
KeyEvent.ACTION_UP -> {
|
||||
ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
|
||||
@ -23,13 +25,16 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
||||
}
|
||||
return true
|
||||
}
|
||||
else if(!isNotFallback){
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun onMotionEvent(ev: MotionEvent) {
|
||||
if(controllerId != -1) {
|
||||
if(true) {
|
||||
if(ev.action == MotionEvent.ACTION_MOVE) {
|
||||
val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
|
||||
val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
|
||||
@ -37,20 +42,60 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
||||
val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
|
||||
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,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)
|
||||
return controllerId
|
||||
}
|
||||
|
||||
fun disconnect(){
|
||||
controllerId = -1
|
||||
}
|
||||
|
||||
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
||||
val quickSettings = QuickSettings(activity)
|
||||
return when (keycode) {
|
||||
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
|
||||
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
|
||||
KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X
|
||||
KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y
|
||||
KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B
|
||||
KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A
|
||||
KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y
|
||||
KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X
|
||||
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
|
||||
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
|
||||
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")
|
||||
class RyujinxNative {
|
||||
external fun initialize(appPath: Long, enableDebugLogs : Boolean): Boolean
|
||||
external fun initialize(appPath: Long): Boolean
|
||||
|
||||
companion object {
|
||||
val instance: RyujinxNative = RyujinxNative()
|
||||
|
||||
init {
|
||||
System.loadLibrary("ryujinx")
|
||||
}
|
||||
}
|
||||
|
||||
external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean,
|
||||
external fun deviceInitialize(
|
||||
isHostMapped: Boolean, useNce: Boolean,
|
||||
systemLanguage: Int,
|
||||
regionCode: Int,
|
||||
enableVsync: Boolean,
|
||||
@ -21,7 +23,9 @@ class RyujinxNative {
|
||||
enablePtc: Boolean,
|
||||
enableInternetAccess: Boolean,
|
||||
timeZone: Long,
|
||||
ignoreMissingServices : Boolean): Boolean
|
||||
ignoreMissingServices: Boolean
|
||||
): Boolean
|
||||
|
||||
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
||||
external fun graphicsInitializeRenderer(
|
||||
extensions: Array<String>,
|
||||
@ -33,9 +37,9 @@ class RyujinxNative {
|
||||
external fun deviceGetGameFrameRate(): Double
|
||||
external fun deviceGetGameFrameTime(): Double
|
||||
external fun deviceGetGameFifo(): Double
|
||||
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo
|
||||
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
|
||||
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean
|
||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||
external fun graphicsRendererRunLoop()
|
||||
@ -49,6 +53,8 @@ class RyujinxNative {
|
||||
external fun inputSetButtonReleased(button: Int, id: Int)
|
||||
external fun inputConnectGamepad(index: Int): 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 deviceCloseEmulation()
|
||||
external fun deviceSignalEmulationClose()
|
||||
@ -64,4 +70,12 @@ class RyujinxNative {
|
||||
external fun userDeleteUser(userId: String)
|
||||
external fun userOpenUser(userId: Long)
|
||||
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)
|
||||
if (path.isNotEmpty()) {
|
||||
data?.apply {
|
||||
var contents = RyujinxNative.instance.deviceGetDlcContentList(
|
||||
val contents = RyujinxNative.instance.deviceGetDlcContentList(
|
||||
NativeHelpers.instance.storeStringJava(path),
|
||||
titleId.toLong(16)
|
||||
)
|
||||
|
@ -1,13 +1,17 @@
|
||||
package org.ryujinx.android.viewmodels
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import org.ryujinx.android.NativeHelpers
|
||||
import org.ryujinx.android.RyujinxNative
|
||||
|
||||
|
||||
class GameModel(var file: DocumentFile, val context: Context) {
|
||||
private var updateDescriptor: ParcelFileDescriptor? = null
|
||||
var type: FileType
|
||||
var descriptor: ParcelFileDescriptor? = null
|
||||
var fileName: String?
|
||||
var fileSize = 0.0
|
||||
@ -19,8 +23,9 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
|
||||
init {
|
||||
fileName = file.name
|
||||
var pid = open()
|
||||
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci"))
|
||||
val pid = open()
|
||||
val ext = NativeHelpers.instance.storeStringJava(file.extension)
|
||||
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext)
|
||||
close()
|
||||
|
||||
fileSize = gameInfo.FileSize
|
||||
@ -29,6 +34,16 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
developer = gameInfo.Developer
|
||||
version = gameInfo.Version
|
||||
icon = gameInfo.Icon
|
||||
type = when {
|
||||
(file.extension == "xci") -> FileType.Xci
|
||||
(file.extension == "nsp") -> FileType.Nsp
|
||||
(file.extension == "nro") -> FileType.Nro
|
||||
else -> FileType.None
|
||||
}
|
||||
|
||||
if (type == FileType.Nro && (titleName.isNullOrEmpty() || titleName == "Unknown")) {
|
||||
titleName = file.name
|
||||
}
|
||||
}
|
||||
|
||||
fun open() : Int {
|
||||
@ -37,13 +52,29 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
||||
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() {
|
||||
descriptor?.close()
|
||||
descriptor = null
|
||||
}
|
||||
|
||||
fun isXci() : Boolean {
|
||||
return file.extension == "xci"
|
||||
updateDescriptor?.close()
|
||||
updateDescriptor = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,3 +86,10 @@ class GameInfo {
|
||||
var Version: 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 com.anggrayudi.storage.file.DocumentFileCompat
|
||||
import com.anggrayudi.storage.file.DocumentFileType
|
||||
import com.anggrayudi.storage.file.FileFullPath
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import com.anggrayudi.storage.file.getAbsolutePath
|
||||
import com.anggrayudi.storage.file.search
|
||||
import org.ryujinx.android.MainActivity
|
||||
import java.util.Locale
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class HomeViewModel(
|
||||
val activity: MainActivity? = null,
|
||||
val mainViewModel: MainViewModel? = null
|
||||
) {
|
||||
private var shouldReload: Boolean = false
|
||||
private var savedFolder: String = ""
|
||||
private var isLoading: Boolean = false
|
||||
private var loadedCache: List<GameModel> = listOf()
|
||||
private var loadedCache: MutableList<GameModel> = mutableListOf()
|
||||
private var gameFolderPath: DocumentFile? = null
|
||||
private var sharedPref: SharedPreferences? = null
|
||||
val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
|
||||
@ -26,46 +27,34 @@ class HomeViewModel(
|
||||
init {
|
||||
if (activity != null) {
|
||||
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()) {
|
||||
try {
|
||||
if (savedFolder.isNotEmpty() && (shouldReload || savedFolder != oldFolder)) {
|
||||
gameFolderPath = DocumentFileCompat.fromFullPath(
|
||||
activity,
|
||||
mainViewModel?.activity!!,
|
||||
savedFolder,
|
||||
documentType = DocumentFileType.FOLDER,
|
||||
requiresWriteAccess = true
|
||||
)
|
||||
|
||||
reloadGameList()
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openGameFolder() {
|
||||
val path = sharedPref?.getString("gameFolder", "") ?: ""
|
||||
fun filter(query : String){
|
||||
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())
|
||||
activity?.storageHelper?.storage?.openFolderPicker()
|
||||
else
|
||||
activity?.storageHelper?.storage?.openFolderPicker(
|
||||
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
||||
FileFullPath(activity, path)
|
||||
)
|
||||
fun requestReload(){
|
||||
shouldReload = true
|
||||
}
|
||||
|
||||
fun reloadGameList() {
|
||||
@ -80,17 +69,19 @@ class HomeViewModel(
|
||||
isLoading = true
|
||||
thread {
|
||||
try {
|
||||
loadedCache.clear()
|
||||
val files = mutableListOf<GameModel>()
|
||||
for (file in folder.search(false, DocumentFileType.FILE)) {
|
||||
if (file.extension == "xci" || file.extension == "nsp")
|
||||
if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro")
|
||||
activity.let {
|
||||
val item = GameModel(file, it)
|
||||
files.add(item)
|
||||
|
||||
if(item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true) {
|
||||
loadedCache.add(item)
|
||||
gameList.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
loadedCache = files.toList()
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
} 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.GameHost
|
||||
import org.ryujinx.android.GraphicsConfiguration
|
||||
import org.ryujinx.android.Logging
|
||||
import org.ryujinx.android.MainActivity
|
||||
import org.ryujinx.android.MotionSensorManager
|
||||
import org.ryujinx.android.NativeGraphicsInterop
|
||||
import org.ryujinx.android.NativeHelpers
|
||||
import org.ryujinx.android.PerformanceManager
|
||||
@ -25,12 +27,15 @@ import java.io.File
|
||||
@SuppressLint("WrongConstant")
|
||||
class MainViewModel(val activity: MainActivity) {
|
||||
var physicalControllerManager: PhysicalControllerManager? = null
|
||||
var motionSensorManager: MotionSensorManager? = null
|
||||
var gameModel: GameModel? = null
|
||||
var controller: GameController? = null
|
||||
var performanceManager: PerformanceManager? = null
|
||||
var selected: GameModel? = null
|
||||
var isMiiEditorLaunched = false
|
||||
val userViewModel = UserViewModel()
|
||||
val logging = Logging(this)
|
||||
var firmwareVersion = ""
|
||||
private var gameTimeState: MutableState<Double>? = null
|
||||
private var gameFpsState: 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 showLoading: MutableState<Boolean>? = null
|
||||
private var refreshUser: MutableState<Boolean>? = null
|
||||
|
||||
var gameHost: GameHost? = null
|
||||
set(value) {
|
||||
field = value
|
||||
@ -59,6 +65,16 @@ class MainViewModel(val activity: MainActivity) {
|
||||
RyujinxNative.instance.deviceSignalEmulationClose()
|
||||
gameHost?.close()
|
||||
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 {
|
||||
@ -69,6 +85,8 @@ class MainViewModel(val activity: MainActivity) {
|
||||
if (descriptor == 0)
|
||||
return false
|
||||
|
||||
val update = game.openUpdate()
|
||||
|
||||
gameModel = game
|
||||
isMiiEditorLaunched = false
|
||||
|
||||
@ -162,7 +180,7 @@ class MainViewModel(val activity: MainActivity) {
|
||||
if (!success)
|
||||
return false
|
||||
|
||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
|
||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
|
||||
|
||||
if (!success)
|
||||
return false
|
||||
@ -170,8 +188,6 @@ class MainViewModel(val activity: MainActivity) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun loadMiiEditor() : Boolean {
|
||||
val nativeRyujinx = RyujinxNative.instance
|
||||
|
||||
@ -351,6 +367,8 @@ class MainViewModel(val activity: MainActivity) {
|
||||
activity.setFullScreen(true)
|
||||
navController?.navigate("game")
|
||||
activity.isGameRunning = true
|
||||
if (QuickSettings(activity).enableMotion)
|
||||
motionSensorManager?.register()
|
||||
}
|
||||
|
||||
fun setProgressStates(
|
||||
|
@ -15,6 +15,19 @@ class QuickSettings(val activity: Activity) {
|
||||
var enableShaderCache: Boolean
|
||||
var enableTextureRecompression: Boolean
|
||||
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)
|
||||
|
||||
@ -29,5 +42,46 @@ class QuickSettings(val activity: Activity) {
|
||||
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
|
||||
resScale = sharedPref.getFloat("resScale", 1f)
|
||||
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 androidx.compose.runtime.MutableState
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.navigation.NavHostController
|
||||
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.NativeHelpers
|
||||
import org.ryujinx.android.RyujinxNative
|
||||
import java.io.File
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
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
|
||||
var selectedFirmwareFile: DocumentFile? = null
|
||||
|
||||
init {
|
||||
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 {
|
||||
@ -27,9 +52,19 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
enableShaderCache: MutableState<Boolean>,
|
||||
enableTextureRecompression: MutableState<Boolean>,
|
||||
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)
|
||||
useNce.value = sharedPref.getBoolean("useNce", true)
|
||||
@ -38,9 +73,22 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
enablePtc.value = sharedPref.getBoolean("enablePtc", true)
|
||||
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
|
||||
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
|
||||
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
|
||||
enableTextureRecompression.value =
|
||||
sharedPref.getBoolean("enableTextureRecompression", false)
|
||||
resScale.value = sharedPref.getFloat("resScale", 1f)
|
||||
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(
|
||||
@ -53,7 +101,18 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
enableShaderCache: MutableState<Boolean>,
|
||||
enableTextureRecompression: MutableState<Boolean>,
|
||||
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()
|
||||
|
||||
@ -67,7 +126,148 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
||||
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
|
||||
editor.putFloat("resScale", resScale.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()
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import com.google.gson.Gson
|
||||
import org.ryujinx.android.Helpers
|
||||
import org.ryujinx.android.MainActivity
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
@ -16,29 +19,32 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
private var basePath: String
|
||||
private var updateJsonName = "updates.json"
|
||||
private var storageHelper: SimpleStorageHelper
|
||||
var currentPaths: MutableList<String> = mutableListOf()
|
||||
var pathsState: SnapshotStateList<String>? = null
|
||||
|
||||
companion object {
|
||||
const val UpdateRequestCode = 1002
|
||||
}
|
||||
|
||||
fun Remove(index: Int) {
|
||||
fun remove(index: Int) {
|
||||
if (index <= 0)
|
||||
return
|
||||
|
||||
data?.paths?.apply {
|
||||
val removed = removeAt(index - 1)
|
||||
File(removed).deleteRecursively()
|
||||
val str = removeAt(index - 1)
|
||||
Uri.parse(str)?.apply {
|
||||
storageHelper.storage.context.contentResolver.releasePersistableUriPermission(
|
||||
this,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
pathsState?.clear()
|
||||
pathsState?.addAll(this)
|
||||
currentPaths = this
|
||||
}
|
||||
}
|
||||
|
||||
fun Add(
|
||||
isCopying: MutableState<Boolean>,
|
||||
copyProgress: MutableState<Float>,
|
||||
currentProgressName: MutableState<String>
|
||||
) {
|
||||
fun add() {
|
||||
val callBack = storageHelper.onFileSelected
|
||||
|
||||
storageHelper.onFileSelected = { requestCode, files ->
|
||||
@ -47,29 +53,29 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
if (requestCode == UpdateRequestCode) {
|
||||
val file = files.firstOrNull()
|
||||
file?.apply {
|
||||
// Copy updates to internal data folder
|
||||
val updatePath = "$basePath/update"
|
||||
File(updatePath).mkdirs()
|
||||
Helpers.copyToData(
|
||||
this,
|
||||
updatePath,
|
||||
storageHelper,
|
||||
isCopying,
|
||||
copyProgress,
|
||||
currentProgressName, ::refreshPaths
|
||||
)
|
||||
if(file.extension == "nsp"){
|
||||
storageHelper.storage.context.contentResolver.takePersistableUriPermission(file.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
currentPaths.add(file.uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
refreshPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
storageHelper.openFilePicker(UpdateRequestCode)
|
||||
}
|
||||
|
||||
fun refreshPaths() {
|
||||
private fun refreshPaths() {
|
||||
data?.apply {
|
||||
val updatePath = "$basePath/update"
|
||||
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)) {
|
||||
selected = ""
|
||||
@ -88,7 +94,6 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
openDialog: MutableState<Boolean>
|
||||
) {
|
||||
data?.apply {
|
||||
val updatePath = "$basePath/update"
|
||||
this.selected = ""
|
||||
if (paths.isNotEmpty() && index > 0) {
|
||||
val ind = max(index - 1, paths.count() - 1)
|
||||
@ -98,18 +103,29 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
File(basePath).mkdirs()
|
||||
|
||||
|
||||
var metadata = TitleUpdateMetadata()
|
||||
val metadata = TitleUpdateMetadata()
|
||||
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
|
||||
|
||||
val selectedName = File(selected).name
|
||||
val newSelectedPath = "$updatePath/$selectedName"
|
||||
if (File(newSelectedPath).exists()) {
|
||||
metadata.selected = newSelectedPath
|
||||
if(selected.isNotEmpty()){
|
||||
val uri = Uri.parse(selected)
|
||||
val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
|
||||
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)
|
||||
|
||||
openDialog.value = false
|
||||
@ -137,10 +153,13 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||
val gson = Gson()
|
||||
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
||||
|
||||
refreshPaths()
|
||||
}
|
||||
|
||||
currentPaths = data?.paths ?: mutableListOf()
|
||||
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.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
@ -51,25 +50,13 @@ class DlcViews {
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Column {
|
||||
Row(modifier = Modifier.padding(8.dp)
|
||||
Row(modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align(
|
||||
Alignment.CenterVertically
|
||||
))
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.add(refresh)
|
||||
},
|
||||
modifier = Modifier.align(
|
||||
Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Add"
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
@ -119,8 +106,18 @@ class DlcViews {
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(modifier = Modifier.align(Alignment.End)) {
|
||||
TextButton(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
modifier = Modifier.padding(4.dp),
|
||||
onClick = {
|
||||
viewModel.add(refresh)
|
||||
}
|
||||
) {
|
||||
|
||||
Text("Add")
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
onClick = {
|
||||
openDialog.value = false
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -82,6 +83,9 @@ class GameViews {
|
||||
val enableVsync = remember {
|
||||
mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync)
|
||||
}
|
||||
val enableMotion = remember {
|
||||
mutableStateOf(QuickSettings(mainViewModel.activity).enableMotion)
|
||||
}
|
||||
val showMore = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@ -169,10 +173,36 @@ class GameViews {
|
||||
modifier = Modifier.padding(16.dp),
|
||||
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 = {
|
||||
showMore.value = false
|
||||
showController.value = !showController.value
|
||||
ryujinxNative.inputReleaseTouchPoint()
|
||||
mainViewModel.controller?.setVisible(showController.value)
|
||||
}) {
|
||||
Icon(
|
||||
@ -183,7 +213,9 @@ class GameViews {
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = false
|
||||
enableVsync.value = !enableVsync.value
|
||||
RyujinxNative.instance.graphicsRendererSetVsync(enableVsync.value)
|
||||
RyujinxNative.instance.graphicsRendererSetVsync(
|
||||
enableVsync.value
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.vSync(),
|
||||
@ -196,6 +228,7 @@ class GameViews {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showBackNotice = remember {
|
||||
mutableStateOf(false)
|
||||
@ -284,6 +317,8 @@ class GameViews {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainViewModel.activity.uiHandler.Compose()
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,7 +336,7 @@ class GameViews {
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(0.4f)
|
||||
color = MaterialTheme.colorScheme.background.copy(0.4f)
|
||||
) {
|
||||
Column {
|
||||
var gameTimeVal = 0.0
|
||||
|
@ -4,6 +4,7 @@ import android.content.res.Resources
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.wrapContentWidth
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
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.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.BottomAppBarDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
@ -50,16 +52,22 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||
import org.ryujinx.android.R
|
||||
import org.ryujinx.android.viewmodels.FileType
|
||||
import org.ryujinx.android.viewmodels.GameModel
|
||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
import kotlin.concurrent.thread
|
||||
@ -67,7 +75,8 @@ import kotlin.math.roundToInt
|
||||
|
||||
class HomeViews {
|
||||
companion object {
|
||||
const val ImageSize = 150
|
||||
const val ListImageSize = 150
|
||||
const val GridImageSize = 300
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -75,11 +84,15 @@ class HomeViews {
|
||||
viewModel: HomeViewModel = HomeViewModel(),
|
||||
navController: NavHostController? = null
|
||||
) {
|
||||
viewModel.ensureReloadIfNecessary()
|
||||
val showAppActions = remember { mutableStateOf(false) }
|
||||
val showLoading = remember { mutableStateOf(false) }
|
||||
val openTitleUpdateDialog = remember { mutableStateOf(false) }
|
||||
val canClose = remember { mutableStateOf(true) }
|
||||
val openDlcDialog = remember { mutableStateOf(false) }
|
||||
val selectedModel = remember {
|
||||
mutableStateOf(viewModel.mainViewModel?.selected)
|
||||
}
|
||||
val query = remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
@ -153,10 +166,134 @@ class HomeViews {
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
}
|
||||
|
||||
) { contentPadding ->
|
||||
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) {
|
||||
IconButton(onClick = {
|
||||
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 = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
viewModel.openGameFolder()
|
||||
},
|
||||
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
|
||||
) {
|
||||
Icon(
|
||||
org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface),
|
||||
contentDescription = "Open Folder"
|
||||
)
|
||||
}
|
||||
onDismissRequest = {
|
||||
showAppActions.value = false
|
||||
selectedModel.value = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
) { 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)
|
||||
@Composable
|
||||
fun GameItem(
|
||||
fun ListGameItem(
|
||||
gameModel: GameModel,
|
||||
viewModel: HomeViewModel,
|
||||
showAppActions: MutableState<Boolean>,
|
||||
@ -400,7 +401,7 @@ class HomeViews {
|
||||
selected = null
|
||||
}
|
||||
selectedModel.value = null
|
||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
|
||||
thread {
|
||||
showLoading.value = true
|
||||
val success =
|
||||
@ -429,10 +430,11 @@ class HomeViews {
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row {
|
||||
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
||||
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
|
||||
if (gameModel.icon?.isNotEmpty() == true) {
|
||||
val pic = decoder.decode(gameModel.icon)
|
||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
||||
val size =
|
||||
ListImageSize / Resources.getSystem().displayMetrics.density
|
||||
Image(
|
||||
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
|
||||
.asImageBitmap(),
|
||||
@ -442,7 +444,9 @@ class HomeViews {
|
||||
.width(size.roundToInt().dp)
|
||||
.height(size.roundToInt().dp)
|
||||
)
|
||||
} else NotAvailableIcon()
|
||||
} else if (gameModel.type == FileType.Nro)
|
||||
NROIcon()
|
||||
else NotAvailableIcon()
|
||||
} else NotAvailableIcon()
|
||||
Column {
|
||||
Text(text = gameModel.titleName ?: "")
|
||||
@ -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
|
||||
fun NotAvailableIcon() {
|
||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
||||
val size = ListImageSize / Resources.getSystem().displayMetrics.density
|
||||
Icon(
|
||||
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
|
||||
.padding(end = 8.dp)
|
||||
.width(size.roundToInt().dp)
|
||||
|
@ -1,6 +1,9 @@
|
||||
package org.ryujinx.android.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
@ -14,6 +17,8 @@ import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -56,6 +61,8 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import org.ryujinx.android.Helpers
|
||||
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.SettingsViewModel
|
||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||
@ -66,7 +73,7 @@ class SettingViews {
|
||||
const val EXPANSTION_TRANSITION_DURATION = 450
|
||||
const val IMPORT_CODE = 12341
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) {
|
||||
val loaded = remember {
|
||||
@ -103,6 +110,27 @@ class SettingViews {
|
||||
val useVirtualController = remember {
|
||||
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) {
|
||||
settingsViewModel.initializeState(
|
||||
@ -112,7 +140,18 @@ class SettingViews {
|
||||
enableShaderCache,
|
||||
enableTextureRecompression,
|
||||
resScale,
|
||||
useVirtualController
|
||||
useVirtualController,
|
||||
isGrid,
|
||||
useSwitchLayout,
|
||||
enableMotion,
|
||||
enableDebugLogs,
|
||||
enableStubLogs,
|
||||
enableInfoLogs,
|
||||
enableWarningLogs,
|
||||
enableErrorLogs,
|
||||
enableGuestLogs,
|
||||
enableAccessLogs,
|
||||
enableTraceLogs
|
||||
)
|
||||
loaded.value = true
|
||||
}
|
||||
@ -134,7 +173,18 @@ class SettingViews {
|
||||
enableShaderCache,
|
||||
enableTextureRecompression,
|
||||
resScale,
|
||||
useVirtualController
|
||||
useVirtualController,
|
||||
isGrid,
|
||||
useSwitchLayout,
|
||||
enableMotion,
|
||||
enableDebugLogs,
|
||||
enableStubLogs,
|
||||
enableInfoLogs,
|
||||
enableWarningLogs,
|
||||
enableErrorLogs,
|
||||
enableGuestLogs,
|
||||
enableAccessLogs,
|
||||
enableTraceLogs
|
||||
)
|
||||
settingsViewModel.navController.popBackStack()
|
||||
}) {
|
||||
@ -145,6 +195,223 @@ class SettingViews {
|
||||
Column(modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.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") {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
@ -330,15 +597,17 @@ class SettingViews {
|
||||
showImportCompletion.value = false
|
||||
importFile.value = null
|
||||
mainViewModel.userViewModel.refreshUsers()
|
||||
mainViewModel.homeViewModel.clearLoadedCache()
|
||||
mainViewModel.homeViewModel.requestReload()
|
||||
}) {
|
||||
Card(
|
||||
modifier = Modifier,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text(modifier = Modifier
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(24.dp),
|
||||
text = "App Data import completed.")
|
||||
text = "App Data import completed."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -591,6 +860,173 @@ class SettingViews {
|
||||
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,
|
||||
enableTextureRecompression,
|
||||
resScale,
|
||||
useVirtualController
|
||||
useVirtualController,
|
||||
isGrid,
|
||||
useSwitchLayout,
|
||||
enableMotion,
|
||||
enableDebugLogs,
|
||||
enableStubLogs,
|
||||
enableInfoLogs,
|
||||
enableWarningLogs,
|
||||
enableErrorLogs,
|
||||
enableGuestLogs,
|
||||
enableAccessLogs,
|
||||
enableTraceLogs
|
||||
)
|
||||
settingsViewModel.navController.popBackStack()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.ryujinx.android.views
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
@ -28,13 +28,19 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.ryujinx.android.MainActivity
|
||||
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
||||
import java.io.File
|
||||
|
||||
class TitleUpdateViews {
|
||||
companion object {
|
||||
@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 selected = remember { mutableStateOf(0) }
|
||||
@ -43,15 +49,6 @@ class TitleUpdateViews {
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
val isCopying = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val copyProgress = remember {
|
||||
mutableStateOf(0.0f)
|
||||
}
|
||||
var currentProgressName = remember {
|
||||
mutableStateOf("Starting Copy")
|
||||
}
|
||||
Column {
|
||||
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
||||
Surface(
|
||||
@ -88,18 +85,24 @@ class TitleUpdateViews {
|
||||
var index = 1
|
||||
for (path in paths) {
|
||||
val i = index
|
||||
val uri = Uri.parse(path)
|
||||
val file = DocumentFile.fromSingleUri(
|
||||
MainActivity.mainViewModel!!.activity,
|
||||
uri
|
||||
)
|
||||
file?.apply {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
RadioButton(
|
||||
selected = (selected.value == i),
|
||||
onClick = { selected.value = i })
|
||||
Text(
|
||||
text = File(path).name,
|
||||
text = file.name ?: "",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
@ -107,7 +110,7 @@ class TitleUpdateViews {
|
||||
Row(modifier = Modifier.align(Alignment.End)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.Remove(selected.value)
|
||||
viewModel.remove(selected.value)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@ -118,7 +121,7 @@ class TitleUpdateViews {
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.Add(isCopying, copyProgress, currentProgressName)
|
||||
viewModel.add()
|
||||
}
|
||||
) {
|
||||
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))
|
||||
TextButton(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
onClick = {
|
||||
if (!isCopying.value) {
|
||||
canClose.value = true
|
||||
viewModel.save(selected.value, openDialog)
|
||||
}
|
||||
},
|
||||
) {
|
||||
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
|
||||
# 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.
|
||||
org.ryujinx.config=debug
|
||||
org.ryujinx.config=release
|
||||
# Controls stripping of symbols from libryujinx
|
||||
# Setting this property to auto causes symbols to be stripped for release builds,
|
||||
# but not for debug builds.
|
||||
@ -33,3 +33,4 @@ org.ryujinx.config=debug
|
||||
org.ryujinx.symbols.strip=auto
|
||||
# Output path of libryujinx.so
|
||||
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