From a583d6bf461a0d9a65442caff2fb7f29b05df4dc Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 28 Oct 2023 15:23:39 +0000 Subject: [PATCH] android - load firmware version at launch clean main ui, add option to import app data android - add basic user management android - fix app menu android - fix game update icon, add app icon android - add crash handler android - fix crash when no user is available at launch android - improve game update selection android - make settings view scrollable, bump version --- src/LibRyujinx/Android/JniExportedMethods.cs | 93 +++ src/RyujinxAndroid/app/build.gradle | 5 +- .../app/src/main/AndroidManifest.xml | 1 - src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h | 1 + .../app/src/main/cpp/ryujinx.cpp | 22 + .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 12207 bytes .../java/org/ryujinx/android/BaseActivity.kt | 16 + .../java/org/ryujinx/android/CrashHandler.kt | 13 + .../java/org/ryujinx/android/GameActivity.kt | 4 +- .../main/java/org/ryujinx/android/Helpers.kt | 145 ++++- .../main/java/org/ryujinx/android/Icons.kt | 183 ++++++ .../java/org/ryujinx/android/MainActivity.kt | 3 +- .../java/org/ryujinx/android/NativeHelpers.kt | 2 + .../java/org/ryujinx/android/RyujinxNative.kt | 11 + .../android/viewmodels/HomeViewModel.kt | 47 +- .../android/viewmodels/MainViewModel.kt | 53 ++ .../viewmodels/TitleUpdateViewModel.kt | 200 ++----- .../org/ryujinx/android/views/HomeViews.kt | 560 +++++++++--------- .../org/ryujinx/android/views/MainView.kt | 3 +- .../org/ryujinx/android/views/SettingViews.kt | 213 +++++-- .../ryujinx/android/views/TitleUpdateViews.kt | 39 +- .../org/ryujinx/android/views/UserViews.kt | 185 ++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 - .../res/drawable/ic_launcher_background.xml | 170 ------ .../res/drawable/ic_launcher_foreground.xml | 35 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 +- .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 2366 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 1460 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 3566 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 5356 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 7524 bytes .../res/values/ic_launcher_background.xml | 4 + 38 files changed, 1333 insertions(+), 715 deletions(-) create mode 100644 src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt create mode 100644 src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt delete mode 100644 src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 src/RyujinxAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index e3a9ab860..255adb456 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -18,6 +18,7 @@ using Silk.NET.Vulkan.Extensions.KHR; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -40,6 +41,9 @@ namespace LibRyujinx [DllImport("libryujinxjni")] private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); + [DllImport("libryujinxjni")] + private extern static void pushString(string ch); + [DllImport("libryujinxjni")] internal extern static void setRenderingThread(); @@ -95,6 +99,12 @@ namespace LibRyujinx return s; } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")] + public static void JniReloadFileSystem() + { + SwitchDevice?.ReloadFileSystem(); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")] public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv, JObjectLocalRef jObj, @@ -503,6 +513,89 @@ namespace LibRyujinx return ConnectGamepad(index); } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")] + public static void JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj) + { + var userId = GetOpenedUser(); + + pushString(userId); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")] + public static JStringLocalRef JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + return CreateString(jEnv, GetUserPicture(userId)); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserPicture")] + public static void JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef picturePtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + var picture = GetString(jEnv, picturePtr) ?? ""; + + SetUserPicture(userId, picture); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserName")] + public static JStringLocalRef JniGetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + return CreateString(jEnv, GetUserName(userId)); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserName")] + public static void JniSetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef userNamePtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + var userName = GetString(jEnv, userNamePtr) ?? ""; + + SetUserName(userId, userName); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetAllUsers")] + public static JArrayLocalRef JniGetAllUsers(JEnvRef jEnv, JObjectLocalRef jObj) + { + var users = GetAllUsers(); + + return CreateStringArray(jEnv, users.ToList()); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userAddUser")] + public static void JniAddUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userNamePtr, JStringLocalRef picturePtr) + { + var userName = GetString(jEnv, userNamePtr) ?? ""; + var picture = GetString(jEnv, picturePtr) ?? ""; + + AddUser(userName, picture); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userDeleteUser")] + public static void JniDeleteUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + DeleteUser(userId); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")] + public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + OpenUser(userId); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userCloseUser")] + public static void JniCloseUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr) + { + var userId = GetString(jEnv, userIdPtr) ?? ""; + + CloseUser(userId); + } + private static FileStream OpenFile(int descriptor) { var safeHandle = new SafeFileHandle(descriptor, false); diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index c1e90b005..a2ecc7554 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "org.ryujinx.android" minSdk 30 targetSdk 33 - versionCode 1 - versionName "1.0" + versionCode 10001 + versionName '1.0.1' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -99,6 +99,7 @@ dependencies { implementation "androidx.preference:preference-ktx:1.2.0" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2' implementation 'com.google.code.gson:gson:2.10.1' + implementation 'net.lingala.zip4j:zip4j:2.11.5' implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0") implementation "io.coil-kt:coil-compose:2.4.0" testImplementation 'junit:junit:4.13.2' diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index 4a05d62e4..73e7d70d3 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -24,7 +24,6 @@ android:icon="@mipmap/ic_launcher" android:isGame="true" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.RyujinxAndroid" tools:targetApi="31"> diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h index b26687b44..87e7c3124 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -45,5 +45,6 @@ long _currentRenderingThreadId = 0; JavaVM* _vm = nullptr; jobject _mainActivity = nullptr; jclass _mainActivityClass = nullptr; +std::string _currentString = ""; #endif //RYUJINXNATIVE_RYUIJNX_H diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index 278c176b5..095f462db 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -311,3 +311,25 @@ JNIEXPORT jstring JNICALL Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) { return createStringFromStdString(env, progressInfo); } + +extern "C" +JNIEXPORT jstring JNICALL +Java_org_ryujinx_android_NativeHelpers_popStringJava(JNIEnv *env, jobject thiz) { + return createStringFromStdString(env, _currentString); +} +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_pushStringJava(JNIEnv *env, jobject thiz, jstring string) { + _currentString = getStringPointer(env, string); +} + + +extern "C" +void pushString(char* str){ + _currentString = str; +} + +extern "C" +const char* popString(){ + return _currentString.c_str(); +} diff --git a/src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png b/src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..e27d7167c2099994cb0e6e022d04ba5e46feec66 GIT binary patch literal 12207 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 T{;2};4shJ?v_Zy^3%C9U6p*$} literal 0 HcmV?d00001 diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt new file mode 100644 index 000000000..d333a3ecc --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt @@ -0,0 +1,16 @@ +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) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt new file mode 100644 index 000000000..2d6503020 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt @@ -0,0 +1,13 @@ +package org.ryujinx.android + +import java.io.File +import java.lang.Thread.UncaughtExceptionHandler + +class CrashHandler : UncaughtExceptionHandler { + var crashLog : String = "" + override fun uncaughtException(t: Thread, e: Throwable) { + crashLog += e.toString() + "\n" + + File(MainActivity.AppPath + "${File.separator}crash.log").writeText(crashLog) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt index 37113912f..1670e879a 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt @@ -6,7 +6,6 @@ import android.content.pm.ActivityInfo import android.os.Bundle import android.view.KeyEvent import android.view.MotionEvent -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement @@ -51,7 +50,7 @@ import org.ryujinx.android.viewmodels.QuickSettings import kotlin.math.abs import kotlin.math.roundToInt -class GameActivity : ComponentActivity() { +class GameActivity : BaseActivity() { private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this) @@ -355,6 +354,7 @@ class GameActivity : ComponentActivity() { .padding(16.dp) ) { Button(onClick = { + showBackNotice.value = false mainViewModel.closeGame() setFullScreen(false) finishActivity(0) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt index 780a8fa6d..aef544c7a 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -7,9 +7,22 @@ import android.net.Uri import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore +import androidx.compose.runtime.MutableState +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.copyFileTo +import com.anggrayudi.storage.file.openInputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.lingala.zip4j.io.inputstream.ZipInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream class Helpers { - companion object{ + companion object { fun getPath(context: Context, uri: Uri): String? { // DocumentProvider @@ -25,7 +38,10 @@ class Helpers { } else if (isDownloadsDocument(uri)) { val id = DocumentsContract.getDocumentId(uri) - val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) return getDataColumn(context, contentUri, null, null) } else if (isMediaDocument(uri)) { val docId = DocumentsContract.getDocumentId(uri) @@ -36,9 +52,11 @@ class Helpers { "image" -> { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI } + "video" -> { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI } + "audio" -> { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI } @@ -54,13 +72,81 @@ class Helpers { } return null } + fun copyToData( + file: DocumentFile, path: String, storageHelper: SimpleStorageHelper, + isCopying: MutableState, + copyProgress: MutableState, + currentProgressName: MutableState, + finish: () -> Unit + ) { + var callback: FileCallback? = object : FileCallback() { + override fun onFailed(errorCode: FileCallback.ErrorCode) { + super.onFailed(errorCode) + File(path).delete() + finish() + } - private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { + override fun onStart(file: Any, workerThread: Thread): Long { + copyProgress.value = 0f + + (file as DocumentFile)?.apply { + currentProgressName.value = "Copying ${file.name}" + } + return super.onStart(file, workerThread) + } + + override fun onReport(report: Report) { + super.onReport(report) + + if(!isCopying.value) { + Thread.currentThread().interrupt() + } + + copyProgress.value = report.progress / 100f + } + + override fun onCompleted(result: Any) { + super.onCompleted(result) + isCopying.value = false + finish() + } + } + val ioScope = CoroutineScope(Dispatchers.IO) + isCopying.value = true + file.apply { + if (!File(path + "/${file.name}").exists()) { + val f = this + ioScope.launch { + f.copyFileTo( + storageHelper.storage.context, + File(path), + callback = callback!! + ) + + } + } + } + } + + private fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array? + ): String? { var cursor: Cursor? = null val column = "_data" val projection = arrayOf(column) try { - cursor = uri?.let { context.contentResolver.query(it, projection, selection, selectionArgs,null) } + cursor = uri?.let { + context.contentResolver.query( + it, + projection, + selection, + selectionArgs, + null + ) + } if (cursor != null && cursor.moveToFirst()) { val column_index: Int = cursor.getColumnIndexOrThrow(column) return cursor.getString(column_index) @@ -82,5 +168,56 @@ class Helpers { private fun isMediaDocument(uri: Uri): Boolean { return "com.android.providers.media.documents" == uri.authority } + + fun importAppData( + file: DocumentFile, + isImporting: MutableState + ) { + isImporting.value = true + try { + MainActivity.StorageHelper?.apply { + val stream = file.openInputStream(storage.context) + stream?.apply { + val folders = listOf("bis", "games", "profiles", "system") + for (f in folders) { + val dir = File(MainActivity.AppPath + "${File.separator}${f}") + if (dir.exists()) { + dir.deleteRecursively() + } + + dir.mkdirs() + } + ZipInputStream(stream).use { zip -> + var count = 0 + while (true) { + val header = zip.nextEntry ?: break + if (!folders.any { header.fileName.startsWith(it) }) { + continue + } + val filePath = + MainActivity.AppPath + File.separator + header.fileName + + if (!header.isDirectory) { + val bos = BufferedOutputStream(FileOutputStream(filePath)) + val bytesIn = ByteArray(4096) + var read: Int = 0 + while (zip.read(bytesIn).also { read = it } > 0) { + bos.write(bytesIn, 0, read) + } + bos.close() + } else { + val dir = File(filePath) + dir.mkdir() + } + } + } + stream.close() + } + } + } finally { + isImporting.value = false + RyujinxNative().deviceReloadFilesystem() + } + } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt index 13820e150..54a508298 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -23,6 +23,189 @@ class Icons { companion object{ /// Icons exported from https://www.composables.com/icons @Composable + fun playArrow(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "play_arrow", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(15.542f, 30f) + quadToRelative(-0.667f, 0.458f, -1.334f, 0.062f) + quadToRelative(-0.666f, -0.395f, -0.666f, -1.187f) + verticalLineTo(10.917f) + quadToRelative(0f, -0.75f, 0.666f, -1.146f) + quadToRelative(0.667f, -0.396f, 1.334f, 0.062f) + lineToRelative(14.083f, 9f) + quadToRelative(0.583f, 0.375f, 0.583f, 1.084f) + quadToRelative(0f, 0.708f, -0.583f, 1.083f) + close() + moveToRelative(0.625f, -10.083f) + close() + moveToRelative(0f, 6.541f) + lineToRelative(10.291f, -6.541f) + lineToRelative(-10.291f, -6.542f) + close() + } + }.build() + } + } + @Composable + fun folderOpen(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "folder_open", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(color), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 33.125f) + quadToRelative(-1.083f, 0f, -1.854f, -0.792f) + quadToRelative(-0.771f, -0.791f, -0.771f, -1.875f) + verticalLineTo(9.667f) + quadToRelative(0f, -1.084f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.771f, 1.854f, -0.771f) + horizontalLineToRelative(10.042f) + quadToRelative(0.541f, 0f, 1.041f, 0.208f) + quadToRelative(0.5f, 0.208f, 0.834f, 0.583f) + lineToRelative(1.875f, 1.834f) + horizontalLineTo(33.75f) + quadToRelative(1.083f, 0f, 1.854f, 0.791f) + quadToRelative(0.771f, 0.792f, 0.771f, 1.834f) + horizontalLineTo(18.917f) + lineTo(16.25f, 9.667f) + horizontalLineToRelative(-10f) + verticalLineTo(30.25f) + lineToRelative(3.542f, -13.375f) + quadToRelative(0.25f, -0.875f, 0.979f, -1.396f) + quadToRelative(0.729f, -0.521f, 1.604f, -0.521f) + horizontalLineToRelative(23.25f) + quadToRelative(1.292f, 0f, 2.104f, 1.021f) + quadToRelative(0.813f, 1.021f, 0.438f, 2.271f) + lineToRelative(-3.459f, 12.833f) + quadToRelative(-0.291f, 1f, -1f, 1.521f) + quadToRelative(-0.708f, 0.521f, -1.75f, 0.521f) + close() + moveToRelative(2.708f, -2.667f) + horizontalLineToRelative(23.167f) + lineToRelative(3.417f, -12.875f) + horizontalLineTo(12.333f) + close() + moveToRelative(0f, 0f) + lineToRelative(3.375f, -12.875f) + lineToRelative(-3.375f, 12.875f) + close() + moveToRelative(-2.708f, -15.5f) + verticalLineTo(9.667f) + verticalLineToRelative(5.291f) + close() + } + }.build() + } + } + @Composable + fun gameUpdate(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "game_update_alt", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), + fillAlpha = 1f, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(6.25f, 33.083f) + quadToRelative(-1.083f, 0f, -1.854f, -0.791f) + quadToRelative(-0.771f, -0.792f, -0.771f, -1.834f) + verticalLineTo(9.542f) + quadToRelative(0f, -1.042f, 0.771f, -1.854f) + quadToRelative(0.771f, -0.813f, 1.854f, -0.813f) + horizontalLineToRelative(8.458f) + quadToRelative(0.584f, 0f, 0.959f, 0.396f) + reflectiveQuadToRelative(0.375f, 0.937f) + quadToRelative(0f, 0.584f, -0.375f, 0.959f) + reflectiveQuadToRelative(-0.959f, 0.375f) + horizontalLineTo(6.25f) + verticalLineToRelative(20.916f) + horizontalLineToRelative(27.542f) + verticalLineTo(9.542f) + horizontalLineToRelative(-8.5f) + quadToRelative(-0.584f, 0f, -0.959f, -0.375f) + reflectiveQuadToRelative(-0.375f, -0.959f) + quadToRelative(0f, -0.541f, 0.375f, -0.937f) + reflectiveQuadToRelative(0.959f, -0.396f) + horizontalLineToRelative(8.5f) + quadToRelative(1.041f, 0f, 1.833f, 0.813f) + quadToRelative(0.792f, 0.812f, 0.792f, 1.854f) + verticalLineToRelative(20.916f) + quadToRelative(0f, 1.042f, -0.792f, 1.834f) + quadToRelative(-0.792f, 0.791f, -1.833f, 0.791f) + close() + moveTo(20f, 25f) + quadToRelative(-0.25f, 0f, -0.479f, -0.083f) + quadToRelative(-0.229f, -0.084f, -0.396f, -0.292f) + lineTo(12.75f, 18.25f) + quadToRelative(-0.375f, -0.333f, -0.375f, -0.896f) + quadToRelative(0f, -0.562f, 0.417f, -0.979f) + quadToRelative(0.375f, -0.375f, 0.916f, -0.375f) + quadToRelative(0.542f, 0f, 0.959f, 0.375f) + lineToRelative(4.041f, 4.083f) + verticalLineTo(8.208f) + quadToRelative(0f, -0.541f, 0.375f, -0.937f) + reflectiveQuadTo(20f, 6.875f) + quadToRelative(0.542f, 0f, 0.938f, 0.396f) + quadToRelative(0.395f, 0.396f, 0.395f, 0.937f) + verticalLineToRelative(12.25f) + lineToRelative(4.084f, -4.083f) + quadToRelative(0.333f, -0.333f, 0.875f, -0.333f) + quadToRelative(0.541f, 0f, 0.916f, 0.375f) + quadToRelative(0.417f, 0.416f, 0.417f, 0.958f) + reflectiveQuadToRelative(-0.375f, 0.917f) + lineToRelative(-6.333f, 6.333f) + quadToRelative(-0.209f, 0.208f, -0.438f, 0.292f) + quadTo(20.25f, 25f, 20f, 25f) + close() + } + }.build() + } + } + @Composable fun download(): ImageVector { val primaryColor = MaterialTheme.colorScheme.primary return remember { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 5f3b95481..29fa53dbf 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -4,7 +4,6 @@ import android.os.Build import android.os.Bundle import android.os.Environment import android.view.WindowManager -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme @@ -17,7 +16,7 @@ import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.views.MainView -class MainActivity : ComponentActivity() { +class MainActivity : BaseActivity() { private var _isInit: Boolean = false var storageHelper: SimpleStorageHelper? = null companion object { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt index eead88176..229b61a90 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -28,4 +28,6 @@ class NativeHelpers { external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int external fun getProgressInfo() : String external fun getProgressValue() : Float + external fun pushStringJava(string: String) + external fun popStringJava() : String } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index 98e54e1e4..4c990f094 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -38,6 +38,7 @@ class RyujinxNative { external fun graphicsRendererSetSize(width: Int, height: Int) external fun graphicsRendererSetVsync(enabled: Boolean) external fun graphicsRendererRunLoop() + external fun deviceReloadFilesystem() external fun inputInitialize(width: Int, height: Int) external fun inputSetClientSize(width: Int, height: Int) external fun inputSetTouchPoint(x: Int, y: Int) @@ -52,4 +53,14 @@ class RyujinxNative { external fun deviceSignalEmulationClose() external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcContentList(path: String, titleId: Long) : Array + external fun userGetOpenedUser() + external fun userGetUserPicture(userId: String) : String + external fun userSetUserPicture(userId: String, picture: String) + external fun userGetUserName(userId: String) : String + external fun userSetUserName(userId: String, userName: String) + external fun userGetAllUsers() : Array + external fun userAddUser(username: String, picture: String) + external fun userDeleteUser(userId: String) + external fun userOpenUser(userId: String) + external fun userCloseUser(userId: String) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt index e8666757f..bbfeb9e8a 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt @@ -68,7 +68,7 @@ class HomeViewModel( ) } - fun reloadGameList() { + fun reloadGameList(ignoreCache: Boolean = false) { var storage = activity?.storageHelper ?: return if(isLoading) @@ -77,27 +77,32 @@ class HomeViewModel( isLoading = true - val files = mutableListOf() + if(!ignoreCache) { + val files = mutableListOf() - thread { - try { - for (file in folder.search(false, DocumentFileType.FILE)) { - if (file.extension == "xci" || file.extension == "nsp") - activity.let { - files.add(GameModel(file, it)) - } + thread { + try { + for (file in folder.search(false, DocumentFileType.FILE)) { + if (file.extension == "xci" || file.extension == "nsp") + activity.let { + files.add(GameModel(file, it)) + } + } + + loadedCache = files.toList() + + isLoading = false + + applyFilter() + } finally { + isLoading = false } - - loadedCache = files.toList() - - isLoading = false - - applyFilter() - } - finally { - isLoading = false } } + else{ + isLoading = false + applyFilter() + } } private fun applyFilter() { @@ -109,6 +114,10 @@ class HomeViewModel( fun setViewList(list: SnapshotStateList) { gameList = list - applyFilter() + reloadGameList(loadedCache.isNotEmpty()) + } + + fun clearLoadedCache(){ + loadedCache = listOf() } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index c7d64ecc0..0d3850425 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -37,6 +37,7 @@ class MainViewModel(val activity: MainActivity) { private var progress: MutableState? = null private var progressValue: MutableState? = null private var showLoading: MutableState? = null + private var refreshUser: MutableState? = null var gameHost: GameHost? = null set(value) { field = value @@ -168,6 +169,47 @@ class MainViewModel(val activity: MainActivity) { return true } + fun clearPptcCache(titleId :String){ + if(titleId.isNotEmpty()){ + val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu" + if(File(basePath).exists()){ + var caches = mutableListOf() + + val mainCache = basePath + "${File.separator}0" + File(mainCache).listFiles()?.forEach { + if(it.isFile && it.name.endsWith(".cache")) + caches.add(it.absolutePath) + } + val backupCache = basePath + "${File.separator}1" + File(backupCache).listFiles()?.forEach { + if(it.isFile && it.name.endsWith(".cache")) + caches.add(it.absolutePath) + } + for(path in caches) + File(path).delete() + } + } + } + + fun purgeShaderCache(titleId :String) { + if(titleId.isNotEmpty()){ + val basePath = MainActivity.AppPath + "/games/$titleId/cache/shader" + if(File(basePath).exists()){ + var caches = mutableListOf() + File(basePath).listFiles()?.forEach { + if(!it.isFile) + it.delete() + else{ + if(it.name.endsWith(".toc") || it.name.endsWith(".data")) + caches.add(it.absolutePath) + } + } + for(path in caches) + File(path).delete() + } + } + } + fun setStatStates( fifo: MutableState, gameFps: MutableState, @@ -213,4 +255,15 @@ class MainViewModel(val activity: MainActivity) { this.progress = progress gameHost?.setProgressStates(showLoading, progressValue, progress) } + + fun setRefreshUserState(refreshUser: MutableState) + { + this.refreshUser = refreshUser + } + + fun requestUserRefresh(){ + refreshUser?.apply { + value = true + } + } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt index 8d3a53456..e42108220 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt @@ -4,27 +4,17 @@ 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.callback.FileCallback -import com.anggrayudi.storage.file.DocumentFileCompat -import com.anggrayudi.storage.file.DocumentFileType -import com.anggrayudi.storage.file.copyFileTo -import com.anggrayudi.storage.file.getAbsolutePath import com.google.gson.Gson -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import org.ryujinx.android.Helpers import org.ryujinx.android.MainActivity import java.io.File -import java.util.LinkedList -import java.util.Queue import kotlin.math.max class TitleUpdateViewModel(val titleId: String) { + private var canClose: MutableState? = null private var basePath: String private var updateJsonName = "updates.json" - private var stagingUpdateJsonName = "staging_updates.json" private var storageHelper: SimpleStorageHelper var pathsState: SnapshotStateList? = null @@ -37,32 +27,37 @@ class TitleUpdateViewModel(val titleId: String) { return data?.paths?.apply { - removeAt(index - 1) + val removed = removeAt(index - 1) + File(removed).deleteRecursively() pathsState?.clear() pathsState?.addAll(this) } } - fun Add() { + fun Add( + isCopying: MutableState, + copyProgress: MutableState, + currentProgressName: MutableState + ) { val callBack = storageHelper.onFileSelected storageHelper.onFileSelected = { requestCode, files -> run { storageHelper.onFileSelected = callBack - if(requestCode == UpdateRequestCode) - { + if (requestCode == UpdateRequestCode) { val file = files.firstOrNull() file?.apply { - val path = file.getAbsolutePath(storageHelper.storage.context) - if(path.isNotEmpty()){ - data?.apply { - if(!paths.contains(path)) { - paths.add(path) - pathsState?.clear() - pathsState?.addAll(paths) - } - } - } + // Copy updates to internal data folder + val updatePath = "$basePath/update" + File(updatePath).mkdirs() + Helpers.copyToData( + this, + updatePath, + storageHelper, + isCopying, + copyProgress, + currentProgressName, ::refreshPaths + ) } } } @@ -70,128 +65,60 @@ class TitleUpdateViewModel(val titleId: String) { storageHelper.openFilePicker(UpdateRequestCode) } + fun refreshPaths() { + data?.apply { + val updatePath = "$basePath/update" + val existingPaths = mutableListOf() + File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) } + + if (!existingPaths.contains(selected)) { + selected = "" + } + pathsState?.clear() + pathsState?.addAll(existingPaths) + paths = existingPaths + canClose?.apply { + value = true + } + } + } + fun save( index: Int, - isCopying: MutableState, - openDialog: MutableState, - copyProgress: MutableState, - currentProgressName: MutableState + openDialog: MutableState ) { data?.apply { + val updatePath = "$basePath/update" this.selected = "" if (paths.isNotEmpty() && index > 0) { val ind = max(index - 1, paths.count() - 1) this.selected = paths[ind] } val gson = Gson() - var json = gson.toJson(this) File(basePath).mkdirs() - File("$basePath/$stagingUpdateJsonName").writeText(json) - // Copy updates to internal data folder - val updatePath = "$basePath/update" - File(updatePath).mkdirs() - - val ioScope = CoroutineScope(Dispatchers.IO) var metadata = TitleUpdateMetadata() - var queue: Queue = LinkedList() + val savedUpdates = mutableListOf() + File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) } + metadata.paths = savedUpdates - var callback: FileCallback? = null - - fun copy(path: String) { - isCopying.value = true - val documentFile = DocumentFileCompat.fromFullPath( - storageHelper.storage.context, - path, - DocumentFileType.FILE - ) - documentFile?.apply { - val stagedPath = "$basePath/${name}" - if (!File(stagedPath).exists()) { - var file = this - ioScope.launch { - file.copyFileTo( - storageHelper.storage.context, - File(updatePath), - callback = callback!! - ) - - } - - metadata.paths.add(stagedPath) - } - } + val selectedName = File(selected).name + val newSelectedPath = "$updatePath/$selectedName" + if (File(newSelectedPath).exists()) { + metadata.selected = newSelectedPath } - fun finish() { - val savedUpdates = mutableListOf() - File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) } - var missingFiles = - savedUpdates.filter { i -> paths.find { it.endsWith(File(i).name) } == null } - for (path in missingFiles) { - File(path).delete() - } + var json = gson.toJson(metadata) + File("$basePath/$updateJsonName").writeText(json) - val selectedName = File(selected).name - val newSelectedPath = "$updatePath/$selectedName" - if (File(newSelectedPath).exists()) { - metadata.selected = newSelectedPath - } - - json = gson.toJson(metadata) - File("$basePath/$updateJsonName").writeText(json) - - openDialog.value = false - isCopying.value = false - } - callback = object : FileCallback() { - override fun onFailed(errorCode: FileCallback.ErrorCode) { - super.onFailed(errorCode) - } - - override fun onStart(file: Any, workerThread: Thread): Long { - copyProgress.value = 0f - - (file as DocumentFile)?.apply { - currentProgressName.value = "Copying ${file.name}" - } - return super.onStart(file, workerThread) - } - - override fun onReport(report: Report) { - super.onReport(report) - - copyProgress.value = report.progress / 100f - } - - override fun onCompleted(result: Any) { - super.onCompleted(result) - - if (queue.isNotEmpty()) - copy(queue.remove()) - else { - finish() - } - } - } - for (path in paths) { - queue.add(path) - } - - ioScope.launch { - if (queue.isNotEmpty()) { - copy(queue.remove()) - } else { - finish() - } - - } + openDialog.value = false } } - fun setPaths(paths: SnapshotStateList) { + fun setPaths(paths: SnapshotStateList, canClose: MutableState) { pathsState = paths + this.canClose = canClose data?.apply { pathsState?.clear() pathsState?.addAll(this.paths) @@ -203,29 +130,14 @@ class TitleUpdateViewModel(val titleId: String) { init { basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) - val stagingJson = "${basePath}/${stagingUpdateJsonName}" jsonPath = "${basePath}/${updateJsonName}" data = TitleUpdateMetadata() - if (File(stagingJson).exists()) { + if (File(jsonPath).exists()) { val gson = Gson() - data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java) + data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) - data?.apply { - val existingPaths = mutableListOf() - for (path in paths) { - if (File(path).exists()) { - existingPaths.add(path) - } - } - - if(!existingPaths.contains(selected)){ - selected = "" - } - pathsState?.clear() - pathsState?.addAll(existingPaths) - paths = existingPaths - } + refreshPaths() } storageHelper = MainActivity.StorageHelper!! diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index 8f22d9e24..f567410f1 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -1,8 +1,9 @@ package org.ryujinx.android.views import android.content.res.Resources -import android.view.Gravity +import android.graphics.BitmapFactory import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth @@ -20,52 +22,51 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Person 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.DockedSearchBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FabPosition 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 import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogWindowProvider -import androidx.compose.ui.zIndex import androidx.navigation.NavHostController import coil.compose.AsyncImage import com.anggrayudi.storage.extension.launchOnUiThread import org.ryujinx.android.MainActivity -import org.ryujinx.android.R +import org.ryujinx.android.NativeHelpers +import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.HomeViewModel import java.io.File +import java.util.Base64 import java.util.Locale import kotlin.concurrent.thread import kotlin.math.roundToInt @@ -76,289 +77,274 @@ class HomeViews { @OptIn(ExperimentalMaterial3Api::class) @Composable - fun MainTopBar( - navController: NavHostController, - query: MutableState, - refresh: MutableState + fun Home( + viewModel: HomeViewModel = HomeViewModel(), + navController: NavHostController? = null ) { - val topBarSize = remember { - mutableStateOf(0) - } - Column { - val showOptionsPopup = remember { - mutableStateOf(false) - } - TopAppBar( - modifier = Modifier - .zIndex(1f) - .padding(top = 8.dp) - .onSizeChanged { - topBarSize.value = it.height - }, - title = { - DockedSearchBar( - shape = SearchBarDefaults.inputFieldShape, - query = query.value, - onQueryChange = { - query.value = it - }, - onSearch = {}, - active = false, - onActiveChange = {}, - leadingIcon = { - Icon( - Icons.Filled.Search, - contentDescription = "Search Games" - ) - }, - placeholder = { - Text(text = "Search Games") - } - ) { - - } - }, - actions = { - IconButton( - onClick = { - refresh.value = true - } - ) { - Icon( - Icons.Filled.Refresh, - contentDescription = "Refresh" - ) - } - IconButton( - onClick = { - showOptionsPopup.value = true - } - ) { - Icon( - Icons.Filled.MoreVert, - contentDescription = "More" - ) - } - } - ) - Box { - if (showOptionsPopup.value) { - AlertDialog( - modifier = Modifier.padding( - top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp, - start = 16.dp, end = 16.dp - ), - onDismissRequest = { - showOptionsPopup.value = false - }) { - val dialogWindowProvider = - LocalView.current.parent as DialogWindowProvider - dialogWindowProvider.window.setGravity(Gravity.TOP) - Surface( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(16.dp), - shape = MaterialTheme.shapes.large, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column { - TextButton( - onClick = { - navController.navigate("settings") - }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Start), - ) { - Icon( - Icons.Filled.Settings, - contentDescription = "Settings" - ) - Text( - text = "Settings", modifier = Modifier - .padding(16.dp) - .align(Alignment.CenterVertically) - ) - } - } - } - } - } - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) { - val sheetState = rememberModalBottomSheetState() - val showBottomSheet = remember { mutableStateOf(false) } + val native = RyujinxNative() + 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 query = remember { mutableStateOf("") } val refresh = remember { mutableStateOf(true) } + val refreshUser = remember { + mutableStateOf(true) + } + + viewModel.mainViewModel?.setRefreshUserState(refreshUser) + val user = remember { + mutableStateOf("") + } + val pic = remember { + mutableStateOf(ByteArray(0)) + } + + if (refreshUser.value) { + native.userGetOpenedUser() + user.value = NativeHelpers().popStringJava() + if (user.value.isNotEmpty()) { + val decoder = Base64.getDecoder() + pic.value = decoder.decode(native.userGetUserPicture(user.value)) + } + + refreshUser.value = false; + } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { - navController?.apply { - MainTopBar(navController, query, refresh) - } + TopAppBar( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + title = { + SearchBar( + modifier = Modifier.fillMaxWidth(), + shape = SearchBarDefaults.inputFieldShape, + query = query.value, + onQueryChange = { + query.value = it + }, + onSearch = {}, + active = false, + onActiveChange = {}, + leadingIcon = { + Icon( + Icons.Filled.Search, + contentDescription = "Search Games" + ) + }, + placeholder = { + Text(text = "Ryujinx") + } + ) { } + }, + actions = { + IconButton(onClick = { + navController?.navigate("user") + }) { + if (pic.value.isNotEmpty()) { + Image( + bitmap = BitmapFactory.decodeByteArray( + pic.value, + 0, + pic.value.size + ) + .asImageBitmap(), + contentDescription = "user image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(52.dp) + .clip(CircleShape) + ) + } else { + Icon( + Icons.Filled.Person, + contentDescription = "user" + ) + } + } + IconButton( + onClick = { + navController?.navigate("settings") + } + ) { + Icon( + Icons.Filled.Settings, + contentDescription = "Settings" + ) + } + } + ) }, - floatingActionButtonPosition = FabPosition.End, - floatingActionButton = { - FloatingActionButton(onClick = { - viewModel.openGameFolder() + bottomBar = { + BottomAppBar(actions = { + if (showAppActions.value) { + IconButton(onClick = { + }) { + Icon( + org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), + contentDescription = "Run" + ) + } + val showAppMenu = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { + showAppMenu.value = true + }) { + Icon( + Icons.Filled.Menu, + contentDescription = "Menu" + ) + } + DropdownMenu( + expanded = showAppMenu.value, + onDismissRequest = { showAppMenu.value = false }) { + DropdownMenuItem(text = { + Text(text = "Clear PPTC Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.clearPptcCache(viewModel.mainViewModel?.selected?.titleId ?: "") + }) + DropdownMenuItem(text = { + Text(text = "Purge Shader Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.purgeShaderCache(viewModel.mainViewModel?.selected?.titleId ?: "") + }) + DropdownMenuItem(text = { + Text(text = "Manage Updates") + }, onClick = { + showAppMenu.value = false + openTitleUpdateDialog.value = true + }) + DropdownMenuItem(text = { + Text(text = "Manage DLC") + }, onClick = { + showAppMenu.value = false + openDlcDialog.value = true + }) + } + } + } }, - shape = CircleShape) { - Icon( - Icons.Filled.Add, - contentDescription = "Options" - ) - } + floatingActionButton = { + FloatingActionButton( + onClick = { + viewModel.openGameFolder() + }, + containerColor = BottomAppBarDefaults.bottomAppBarFabColor, + elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() + ) { + Icon( + org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface), + contentDescription = "Open Folder" + ) + } + } + ) } + ) { contentPadding -> Box(modifier = Modifier.padding(contentPadding)) { val list = remember { mutableStateListOf() } - - - if(refresh.value) { + if (refresh.value) { viewModel.setViewList(list) refresh.value = false + showAppActions.value = false + } + val selectedModel = remember { + mutableStateOf(viewModel.mainViewModel?.selected) } LazyColumn(Modifier.fillMaxSize()) { items(list) { it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim().isEmpty() || this.lowercase( + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase( Locale.getDefault() ) - .contains(query.value))) - GameItem(it, viewModel, showBottomSheet, showLoading) + .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 + if (showLoading.value) { + AlertDialog(onDismissRequest = { }) { + Card( + modifier = Modifier .padding(16.dp) - .fillMaxWidth()) { - Text(text = "Loading") - LinearProgressIndicator(modifier = Modifier + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) .fillMaxWidth() - .padding(top = 16.dp)) + ) { + Text(text = "Loading") + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) } } } } - - if(showBottomSheet.value) { - ModalBottomSheet(onDismissRequest = { - showBottomSheet.value = false - }, - sheetState = sheetState) { - val openTitleUpdateDialog = remember { mutableStateOf(false) } - val openDlcDialog = remember { mutableStateOf(false) } - 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) - } - - } + 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) - } - } } - Surface(color = MaterialTheme.colorScheme.surface, - modifier = Modifier.padding(16.dp)) { - Column(modifier = Modifier.fillMaxSize()) { - Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { - Card( - modifier = Modifier.padding(8.dp), - onClick = { - openTitleUpdateDialog.value = true - } - ) { - Column(modifier = Modifier.padding(16.dp)) { - Icon( - painter = painterResource(R.drawable.app_update), - contentDescription = "Game Updates", - tint = Color.Green, - modifier = Modifier - .width(48.dp) - .height(48.dp) - .align(Alignment.CenterHorizontally) - ) - Text(text = "Game Updates", - modifier = Modifier.align(Alignment.CenterHorizontally), - color = MaterialTheme.colorScheme.onSurface) - - } - } - Card( - modifier = Modifier.padding(8.dp), - onClick = { - openDlcDialog.value = true - } - ) { - Column(modifier = Modifier.padding(16.dp)) { - Icon( - imageVector = org.ryujinx.android.Icons.download(), - contentDescription = "Game Dlc", - tint = Color.Green, - modifier = Modifier - .width(48.dp) - .height(48.dp) - .align(Alignment.CenterHorizontally) - ) - Text(text = "Game DLC", - modifier = Modifier.align(Alignment.CenterHorizontally), - color = MaterialTheme.colorScheme.onSurface) - - } - } - } - } + } + 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) } + } } } @@ -369,16 +355,31 @@ class HomeViews { fun GameItem( gameModel: GameModel, viewModel: HomeViewModel, - showSheet: MutableState, - showLoading: MutableState + showAppActions: MutableState, + showLoading: MutableState, + selectedModel: MutableState ) { - Surface(shape = MaterialTheme.shapes.medium, + remember { + selectedModel + } + val color = + if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + + Surface( + shape = MaterialTheme.shapes.medium, + color = color, modifier = Modifier .fillMaxWidth() .padding(8.dp) .combinedClickable( onClick = { - if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { + if (viewModel.mainViewModel?.selected != null) { + showAppActions.value = false + viewModel.mainViewModel?.apply { + selected = null + } + selectedModel.value = null + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { thread { showLoading.value = true val success = @@ -396,35 +397,40 @@ class HomeViews { }, onLongClick = { viewModel.mainViewModel?.selected = gameModel - showSheet.value = true - })) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.SpaceBetween) { + showAppActions.value = true + selectedModel.value = gameModel + }) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { Row { - if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") - { - val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache + if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { + val iconSource = + MainActivity.AppPath + "/iconCache/" + gameModel.iconCache val imageFile = File(iconSource) - if(imageFile.exists()) { + if (imageFile.exists()) { val size = ImageSize / Resources.getSystem().displayMetrics.density - AsyncImage(model = imageFile, + AsyncImage( + model = imageFile, contentDescription = gameModel.titleName + " icon", - modifier = Modifier - .padding(end = 8.dp) - .width(size.roundToInt().dp) - .height(size.roundToInt().dp)) - } - else NotAvailableIcon() + modifier = Modifier + .padding(end = 8.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } else NotAvailableIcon() } else NotAvailableIcon() - Column{ + Column { Text(text = gameModel.titleName ?: "") Text(text = gameModel.developer ?: "") Text(text = gameModel.titleId ?: "") } } - Column{ + Column { Text(text = gameModel.version ?: "") Text(text = String.format("%.3f", gameModel.fileSize)) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index 7c5c988e1..b613cc51d 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -16,12 +16,13 @@ class MainView { NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } + composable("user") { UserViews.Main(mainViewModel, navController) } composable("settings") { SettingViews.Main( SettingsViewModel( navController, mainViewModel.activity - ) + ), mainViewModel ) } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index f7cc010c0..d1d00251c 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -3,9 +3,7 @@ package org.ryujinx.android.views import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition @@ -23,6 +21,8 @@ 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.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.KeyboardArrowUp @@ -33,6 +33,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api 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.Scaffold @@ -51,16 +52,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +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.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.VulkanDriverViewModel +import kotlin.concurrent.thread class SettingViews { companion object { const val EXPANSTION_TRANSITION_DURATION = 450 + const val IMPORT_CODE = 12341 @OptIn(ExperimentalMaterial3Api::class) @Composable - fun Main(settingsViewModel: SettingsViewModel) { + fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) { val loaded = remember { mutableStateOf(false) } @@ -134,7 +142,9 @@ class SettingViews { } }) }) { contentPadding -> - Column(modifier = Modifier.padding(contentPadding)) { + Column(modifier = Modifier + .padding(contentPadding) + .verticalScroll(rememberScrollState())) { ExpandableView(onCardArrowClick = { }, title = "System") { Column(modifier = Modifier.fillMaxWidth()) { Row( @@ -227,6 +237,121 @@ class SettingViews { ignoreMissingServices.value = !ignoreMissingServices.value }) } + val isImporting = remember { + mutableStateOf(false) + } + val showImportWarning = remember { + mutableStateOf(false) + } + val showImportCompletion = remember { + mutableStateOf(false) + } + var importFile = remember { + mutableStateOf(null) + } + Button(onClick = { + val storage = MainActivity.StorageHelper + storage?.apply { + val s = this.storage + val callBack = this.onFileSelected + onFileSelected = { requestCode, files -> + run { + onFileSelected = callBack + if (requestCode == IMPORT_CODE) { + val file = files.firstOrNull() + file?.apply { + if (this.extension == "zip") { + importFile.value = this + showImportWarning.value = true + } + } + } + } + } + openFilePicker( + IMPORT_CODE, + filterMimeTypes = arrayOf("application/zip") + ) + } + }) { + Text(text = "Import App Data") + } + + if (showImportWarning.value) { + AlertDialog(onDismissRequest = { + showImportWarning.value = false + importFile.value = null + }) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = "Importing app data will delete your current profile. Do you still want to continue?") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + val file = importFile.value + showImportWarning.value = false + importFile.value = null + file?.apply { + thread { + Helpers.importAppData(this, isImporting) + showImportCompletion.value = true + mainViewModel.requestUserRefresh() + } + } + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Yes") + } + Button(onClick = { + showImportWarning.value = false + importFile.value = null + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "No") + } + } + } + + } + } + } + + if (showImportCompletion.value) { + AlertDialog(onDismissRequest = { + showImportCompletion.value = false + importFile.value = null + mainViewModel.requestUserRefresh() + mainViewModel.homeViewModel.clearLoadedCache() + }) { + Card( + modifier = Modifier, + shape = MaterialTheme.shapes.medium + ) { + Text(modifier = Modifier + .padding(24.dp), + text = "App Data import completed.") + } + } + } + + if (isImporting.value) { + Text(text = "Importing Files") + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } } } ExpandableView(onCardArrowClick = { }, title = "Graphics") { @@ -257,14 +382,14 @@ class SettingViews { text = "Resolution Scale", modifier = Modifier.align(Alignment.CenterVertically) ) - Text(text = resScale.value.toString() +"x") + Text(text = resScale.value.toString() + "x") } Slider(value = resScale.value, valueRange = 0.5f..4f, steps = 6, onValueChange = { it -> resScale.value = it - } ) + }) Row( modifier = Modifier .fillMaxWidth() @@ -276,9 +401,12 @@ class SettingViews { text = "Enable Texture Recompression", modifier = Modifier.align(Alignment.CenterVertically) ) - Switch(checked = enableTextureRecompression.value, onCheckedChange = { - enableTextureRecompression.value = !enableTextureRecompression.value - }) + Switch( + checked = enableTextureRecompression.value, + onCheckedChange = { + enableTextureRecompression.value = + !enableTextureRecompression.value + }) } Row( modifier = Modifier @@ -290,7 +418,8 @@ class SettingViews { var isDriverSelectorOpen = remember { mutableStateOf(false) } - var driverViewModel = VulkanDriverViewModel(settingsViewModel.activity) + var driverViewModel = + VulkanDriverViewModel(settingsViewModel.activity) var isChanged = remember { mutableStateOf(false) } @@ -302,16 +431,16 @@ class SettingViews { mutableStateOf(0) } - if(refresh.value) { + if (refresh.value) { isChanged.value = true refresh.value = false } - if(isDriverSelectorOpen.value){ + if (isDriverSelectorOpen.value) { AlertDialog(onDismissRequest = { isDriverSelectorOpen.value = false - if(isChanged.value){ + if (isChanged.value) { driverViewModel.saveSelected() } }) { @@ -329,11 +458,15 @@ class SettingViews { isChanged.value = true } Column { - Column (modifier = Modifier - .fillMaxWidth() - .height(300.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(300.dp) + ) { Row( - modifier = Modifier.fillMaxWidth().padding(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( @@ -359,7 +492,9 @@ class SettingViews { for (driver in drivers) { var ind = driverIndex Row( - modifier = Modifier.fillMaxWidth().padding(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( @@ -378,15 +513,21 @@ class SettingViews { driverViewModel.selected = driver.driverPath }) { - Text(text = driver.libraryName, + Text( + text = driver.libraryName, modifier = Modifier - .fillMaxWidth()) - Text(text = driver.driverVersion, + .fillMaxWidth() + ) + Text( + text = driver.driverVersion, modifier = Modifier - .fillMaxWidth()) - Text(text = driver.description, + .fillMaxWidth() + ) + Text( + text = driver.description, modifier = Modifier - .fillMaxWidth()) + .fillMaxWidth() + ) } } @@ -425,7 +566,7 @@ class SettingViews { isDriverSelectorOpen.value = !isDriverSelectorOpen.value }, modifier = Modifier.align(Alignment.CenterVertically) - ){ + ) { Text(text = "Drivers") } } @@ -485,24 +626,6 @@ class SettingViews { } } val transition = updateTransition(transitionState, label = "transition") - val cardPaddingHorizontal by transition.animateDp({ - tween(durationMillis = EXPANSTION_TRANSITION_DURATION) - }, label = "paddingTransition") { - if (mutableExpanded.value) 48.dp else 24.dp - } - val cardElevation by transition.animateDp({ - tween(durationMillis = EXPANSTION_TRANSITION_DURATION) - }, label = "elevationTransition") { - if (mutableExpanded.value) 24.dp else 4.dp - } - val cardRoundedCorners by transition.animateDp({ - tween( - durationMillis = EXPANSTION_TRANSITION_DURATION, - easing = FastOutSlowInEasing - ) - }, label = "cornersTransition") { - if (mutableExpanded.value) 0.dp else 16.dp - } val arrowRotationDegree by transition.animateFloat({ tween(durationMillis = EXPANSTION_TRANSITION_DURATION) }, label = "rotationDegreeTransition") { @@ -514,7 +637,7 @@ class SettingViews { modifier = Modifier .fillMaxWidth() .padding( - horizontal = cardPaddingHorizontal, + horizontal = 24.dp, vertical = 8.dp ) ) { @@ -600,4 +723,4 @@ class SettingViews { } } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt index 558bc559b..96347f259 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -27,11 +27,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.ryujinx.android.viewmodels.TitleUpdateViewModel +import java.io.File class TitleUpdateViews { companion object { @Composable - fun Main(titleId: String, name: String, openDialog: MutableState) { + fun Main(titleId: String, name: String, openDialog: MutableState, canClose: MutableState) { val viewModel = TitleUpdateViewModel(titleId) val selected = remember { mutableStateOf(0) } @@ -46,6 +47,9 @@ class TitleUpdateViews { val copyProgress = remember { mutableStateOf(0.0f) } + var currentProgressName = remember { + mutableStateOf("Starting Copy") + } Column { Text(text = "Updates for ${name}", textAlign = TextAlign.Center) Surface( @@ -77,7 +81,7 @@ class TitleUpdateViews { mutableStateListOf() } - viewModel.setPaths(paths) + viewModel.setPaths(paths, canClose) var index = 1 for (path in paths) { val i = index @@ -86,7 +90,7 @@ class TitleUpdateViews { selected = (selected.value == i), onClick = { selected.value = i }) Text( - text = path, + text = File(path).name, modifier = Modifier .fillMaxWidth() .align(Alignment.CenterVertically) @@ -111,7 +115,7 @@ class TitleUpdateViews { IconButton( onClick = { - viewModel.Add() + viewModel.Add(isCopying, copyProgress, currentProgressName) } ) { Icon( @@ -122,22 +126,33 @@ class TitleUpdateViews { } } - var currentProgressName = remember { - mutableStateOf("Starting Copy") - } if (isCopying.value) { Text(text = "Copying updates to local storage") Text(text = currentProgressName.value) - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - progress = copyProgress.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 = { - viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName) + if (!isCopying.value) { + canClose.value = true + viewModel.save(selected.value, openDialog) + } }, ) { Text("Save") diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt new file mode 100644 index 000000000..63f0f2379 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/UserViews.kt @@ -0,0 +1,185 @@ +package org.ryujinx.android.views + +import android.graphics.BitmapFactory +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import org.ryujinx.android.NativeHelpers +import org.ryujinx.android.RyujinxNative +import org.ryujinx.android.viewmodels.MainViewModel +import java.util.Base64 + +class UserViews { + companion object { + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + @Composable + fun Main(viewModel: MainViewModel? = null, navController: NavHostController? = null) { + val ryujinxNative = RyujinxNative() + val decoder = Base64.getDecoder() + ryujinxNative.userGetOpenedUser() + val openedUser = remember { + mutableStateOf(NativeHelpers().popStringJava()) + } + + val openedUserPic = remember { + mutableStateOf(decoder.decode(ryujinxNative.userGetUserPicture(openedUser.value))) + } + val openedUserName = remember { + mutableStateOf(ryujinxNative.userGetUserName(openedUser.value)) + } + + val userList = remember { + mutableListOf("") + } + + fun refresh() { + userList.clear() + userList.addAll(ryujinxNative.userGetAllUsers()) + } + + refresh() + + Scaffold(modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(title = { + Text(text = "Users") + }, + navigationIcon = { + IconButton(onClick = { + viewModel?.navController?.popBackStack() + }) { + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + } + }) + }) { contentPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = "Selected user") + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + bitmap = BitmapFactory.decodeByteArray( + openedUserPic.value, + 0, + openedUserPic.value.size + ).asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(4.dp) + .size(96.dp) + .clip(CircleShape) + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = openedUserName.value) + Text(text = openedUser.value) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Available Users") + IconButton(onClick = { + refresh() + }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = "refresh users" + ) + } + } + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + ) { + items(userList) { user -> + val pic = decoder.decode(ryujinxNative.userGetUserPicture(user)) + val name = ryujinxNative.userGetUserName(user) + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), + contentDescription = "selected image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .clip(CircleShape) + .align(Alignment.CenterHorizontally) + .combinedClickable( + onClick = { + ryujinxNative.userOpenUser(user) + openedUser.value = user + openedUserPic.value = pic + openedUserName.value = name + viewModel?.requestUserRefresh() + }) + ) + } + } + } + + } + } + } + + } + + @Preview + @Composable + fun Preview() { + UserViews.Main() + } +} diff --git a/src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d114..000000000 --- a/src/RyujinxAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cb..000000000 --- a/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..c4dd11353 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755bf..7353dbd1f 100644 --- a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755bf..7353dbd1f 100644 --- a/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/src/RyujinxAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..3a8b2257a2bcf4b7aa21eb1b0a81fc7b5fbcabf5 100644 GIT binary patch literal 2366 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 diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!T#sy>t%*i%#GE7D}8PS~SXJ%$*W@c<=N$;RiW!Dbgc7LsM$*yvQT-mlw z+nRG>?`zv;WIbiJZCjbm53v1y!Nzrd6hGTgT^@x*S2kA&;D&y{!7Kl1K76F+O}=m<_n~@Rob?lIBX+Hk?NTF zo-zLbmvtk=11iDz!3+`7E!8rq)Q&jbW6;8ZP6R z@We;_yYMBcYIugrN{|$Xxlv2TMd5Dv5#Z5j8Z0tVD)J#ys^D55>X~`xY=@5KKNaBNOli56h1vWW zDRo34nPB23$N+)d_Q1daD|JAY-08UOnSk+^c{?c`^(}U5cZtO9Vvhix0m8)DN=3UU zza_x)z@Br5eX9}Uzu?236$QL6&;mxbgJK(M&9{fZ!-n~f{npK>`JV#Vy}97GZV>ft zD5QlR4`+b;_0ELj*NE|-3OHkk@r8HrTCApAErdVJ_`Q|-1et|eR<=wEKSFZ<^Q*lxd>6idOaHUatq-6G8wzt#LcI9C` z^J3cbt)5lMPBt=%jIv;>+uS1g{uoE}M;CI@fUAi5PK zuM80U6+4zA!5?{%4LBK_-LJKf6KfsH1t4cX_aFg81@*_}9HMtiosYW$`b>fsCMX;} zxd9!i!5?q*@UF}fp+p(nES}y0rltg4ZJPgP-nJYMFO>C(k?&_ZK`$t>ko5N>bH)s~ zUu{h|eyMWCzIuvOkrkm{-sDi024J`0pvXD889cB9DuP6z?ZqQub3qG#%k8!7#%;co z3<7?RY$^j$NQ#Jie&+&$^9}&af85OWP9_$|Lkq1RHyac!b{CGV3cc;hAmzCi|vVqPz*2y`r~+Rxii3Qhv- zZ_p?3!W|UslSMlXp;0~q?kDgK2(ScU9oHHM4;9dNOLa;KAC<4%ng-#FbNMV6)fAq; zgDUthD0SWxD7){($X}y#W;xq((b&I#?>w2{zo3+LxYk3#iFscPOe28K8E3ugr0f?> zZ}9oR@dZn3y}PBdlkfJBj`}+e{t%vmM^$<2Y}>l3Yzpb-AvBbCdj%-hcQt(Et!TQq z^P{K#e4WMuJOYTSs(Ln8S2-x>=?Ckbgvj%YL=@=p)QkOP@j2sdlll5 Oh~)laszlw8L74%L+RcXm literal 1772 zcmVQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/RyujinxAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..94f9c720865b443506280d33cf11417d5453f9d2 100644 GIT binary patch literal 3566 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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 literal 7778 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml b/src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..c5d5899fd --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file