1
0
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:
Emmanuel Hansen 2023-12-30 12:35:38 +00:00
parent eac63c756e
commit 28df6f1c4e
36 changed files with 2607 additions and 440 deletions

View File

@ -1,4 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Formatters;
using Ryujinx.Common.Logging.Targets;
using System;

View 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();
}
}
}

View File

@ -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)
{

View File

@ -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'

View File

@ -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>

View File

@ -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

View File

@ -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;
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
})
}

View File

@ -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()

View File

@ -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,

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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?) {
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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")
}
}
}
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
)

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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(

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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 {
}
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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()
}

View File

@ -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")

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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>

View File

@ -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