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
@ -18,6 +18,7 @@ using Silk.NET.Vulkan.Extensions.KHR;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@ -40,6 +41,9 @@ namespace LibRyujinx
|
|||||||
[DllImport("libryujinxjni")]
|
[DllImport("libryujinxjni")]
|
||||||
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
|
||||||
|
|
||||||
|
[DllImport("libryujinxjni")]
|
||||||
|
private extern static void pushString(string ch);
|
||||||
|
|
||||||
[DllImport("libryujinxjni")]
|
[DllImport("libryujinxjni")]
|
||||||
internal extern static void setRenderingThread();
|
internal extern static void setRenderingThread();
|
||||||
|
|
||||||
@ -95,6 +99,12 @@ namespace LibRyujinx
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")]
|
||||||
|
public static void JniReloadFileSystem()
|
||||||
|
{
|
||||||
|
SwitchDevice?.ReloadFileSystem();
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")]
|
||||||
public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv,
|
public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv,
|
||||||
JObjectLocalRef jObj,
|
JObjectLocalRef jObj,
|
||||||
@ -503,6 +513,89 @@ namespace LibRyujinx
|
|||||||
return ConnectGamepad(index);
|
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)
|
private static FileStream OpenFile(int descriptor)
|
||||||
{
|
{
|
||||||
var safeHandle = new SafeFileHandle(descriptor, false);
|
var safeHandle = new SafeFileHandle(descriptor, false);
|
||||||
|
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "org.ryujinx.android"
|
applicationId "org.ryujinx.android"
|
||||||
minSdk 30
|
minSdk 30
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 1
|
versionCode 10001
|
||||||
versionName "1.0"
|
versionName '1.0.1'
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -99,6 +99,7 @@ dependencies {
|
|||||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
|
||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
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("br.com.devsrsouza.compose.icons:css-gg:1.1.0")
|
||||||
implementation "io.coil-kt:coil-compose:2.4.0"
|
implementation "io.coil-kt:coil-compose:2.4.0"
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.RyujinxAndroid"
|
android:theme="@style/Theme.RyujinxAndroid"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
@ -45,5 +45,6 @@ long _currentRenderingThreadId = 0;
|
|||||||
JavaVM* _vm = nullptr;
|
JavaVM* _vm = nullptr;
|
||||||
jobject _mainActivity = nullptr;
|
jobject _mainActivity = nullptr;
|
||||||
jclass _mainActivityClass = nullptr;
|
jclass _mainActivityClass = nullptr;
|
||||||
|
std::string _currentString = "";
|
||||||
|
|
||||||
#endif //RYUJINXNATIVE_RYUIJNX_H
|
#endif //RYUJINXNATIVE_RYUIJNX_H
|
||||||
|
@ -311,3 +311,25 @@ JNIEXPORT jstring JNICALL
|
|||||||
Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
|
Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
|
||||||
return createStringFromStdString(env, progressInfo);
|
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();
|
||||||
|
}
|
||||||
|
BIN
src/RyujinxAndroid/app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import android.content.pm.ActivityInfo
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -51,7 +50,7 @@ import org.ryujinx.android.viewmodels.QuickSettings
|
|||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class GameActivity : ComponentActivity() {
|
class GameActivity : BaseActivity() {
|
||||||
private var physicalControllerManager: PhysicalControllerManager =
|
private var physicalControllerManager: PhysicalControllerManager =
|
||||||
PhysicalControllerManager(this)
|
PhysicalControllerManager(this)
|
||||||
|
|
||||||
@ -355,6 +354,7 @@ class GameActivity : ComponentActivity() {
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
|
showBackNotice.value = false
|
||||||
mainViewModel.closeGame()
|
mainViewModel.closeGame()
|
||||||
setFullScreen(false)
|
setFullScreen(false)
|
||||||
finishActivity(0)
|
finishActivity(0)
|
||||||
|
@ -7,6 +7,19 @@ import android.net.Uri
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.MediaStore
|
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 {
|
class Helpers {
|
||||||
companion object {
|
companion object {
|
||||||
@ -25,7 +38,10 @@ class Helpers {
|
|||||||
|
|
||||||
} else if (isDownloadsDocument(uri)) {
|
} else if (isDownloadsDocument(uri)) {
|
||||||
val id = DocumentsContract.getDocumentId(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)
|
return getDataColumn(context, contentUri, null, null)
|
||||||
} else if (isMediaDocument(uri)) {
|
} else if (isMediaDocument(uri)) {
|
||||||
val docId = DocumentsContract.getDocumentId(uri)
|
val docId = DocumentsContract.getDocumentId(uri)
|
||||||
@ -36,9 +52,11 @@ class Helpers {
|
|||||||
"image" -> {
|
"image" -> {
|
||||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
}
|
}
|
||||||
|
|
||||||
"video" -> {
|
"video" -> {
|
||||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
}
|
}
|
||||||
|
|
||||||
"audio" -> {
|
"audio" -> {
|
||||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||||
}
|
}
|
||||||
@ -54,13 +72,81 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
fun copyToData(
|
||||||
|
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
|
||||||
|
isCopying: MutableState<Boolean>,
|
||||||
|
copyProgress: MutableState<Float>,
|
||||||
|
currentProgressName: MutableState<String>,
|
||||||
|
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>?): 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>?
|
||||||
|
): String? {
|
||||||
var cursor: Cursor? = null
|
var cursor: Cursor? = null
|
||||||
val column = "_data"
|
val column = "_data"
|
||||||
val projection = arrayOf(column)
|
val projection = arrayOf(column)
|
||||||
try {
|
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()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
val column_index: Int = cursor.getColumnIndexOrThrow(column)
|
val column_index: Int = cursor.getColumnIndexOrThrow(column)
|
||||||
return cursor.getString(column_index)
|
return cursor.getString(column_index)
|
||||||
@ -82,5 +168,56 @@ class Helpers {
|
|||||||
private fun isMediaDocument(uri: Uri): Boolean {
|
private fun isMediaDocument(uri: Uri): Boolean {
|
||||||
return "com.android.providers.media.documents" == uri.authority
|
return "com.android.providers.media.documents" == uri.authority
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun importAppData(
|
||||||
|
file: DocumentFile,
|
||||||
|
isImporting: MutableState<Boolean>
|
||||||
|
) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,189 @@ class Icons {
|
|||||||
companion object{
|
companion object{
|
||||||
/// Icons exported from https://www.composables.com/icons
|
/// Icons exported from https://www.composables.com/icons
|
||||||
@Composable
|
@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 {
|
fun download(): ImageVector {
|
||||||
val primaryColor = MaterialTheme.colorScheme.primary
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
return remember {
|
return remember {
|
||||||
|
@ -4,7 +4,6 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -17,7 +16,7 @@ import org.ryujinx.android.viewmodels.MainViewModel
|
|||||||
import org.ryujinx.android.views.MainView
|
import org.ryujinx.android.views.MainView
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
private var _isInit: Boolean = false
|
private var _isInit: Boolean = false
|
||||||
var storageHelper: SimpleStorageHelper? = null
|
var storageHelper: SimpleStorageHelper? = null
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -28,4 +28,6 @@ class NativeHelpers {
|
|||||||
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
|
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
|
||||||
external fun getProgressInfo() : String
|
external fun getProgressInfo() : String
|
||||||
external fun getProgressValue() : Float
|
external fun getProgressValue() : Float
|
||||||
|
external fun pushStringJava(string: String)
|
||||||
|
external fun popStringJava() : String
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ class RyujinxNative {
|
|||||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||||
external fun graphicsRendererRunLoop()
|
external fun graphicsRendererRunLoop()
|
||||||
|
external fun deviceReloadFilesystem()
|
||||||
external fun inputInitialize(width: Int, height: Int)
|
external fun inputInitialize(width: Int, height: Int)
|
||||||
external fun inputSetClientSize(width: Int, height: Int)
|
external fun inputSetClientSize(width: Int, height: Int)
|
||||||
external fun inputSetTouchPoint(x: Int, y: Int)
|
external fun inputSetTouchPoint(x: Int, y: Int)
|
||||||
@ -52,4 +53,14 @@ class RyujinxNative {
|
|||||||
external fun deviceSignalEmulationClose()
|
external fun deviceSignalEmulationClose()
|
||||||
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
|
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
|
||||||
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
|
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<String>
|
||||||
|
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<String>
|
||||||
|
external fun userAddUser(username: String, picture: String)
|
||||||
|
external fun userDeleteUser(userId: String)
|
||||||
|
external fun userOpenUser(userId: String)
|
||||||
|
external fun userCloseUser(userId: String)
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ class HomeViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGameList() {
|
fun reloadGameList(ignoreCache: Boolean = false) {
|
||||||
var storage = activity?.storageHelper ?: return
|
var storage = activity?.storageHelper ?: return
|
||||||
|
|
||||||
if(isLoading)
|
if(isLoading)
|
||||||
@ -77,6 +77,7 @@ class HomeViewModel(
|
|||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
|
if(!ignoreCache) {
|
||||||
val files = mutableListOf<GameModel>()
|
val files = mutableListOf<GameModel>()
|
||||||
|
|
||||||
thread {
|
thread {
|
||||||
@ -93,12 +94,16 @@ class HomeViewModel(
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
applyFilter()
|
applyFilter()
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
isLoading = false
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyFilter() {
|
private fun applyFilter() {
|
||||||
if(isLoading)
|
if(isLoading)
|
||||||
@ -109,6 +114,10 @@ class HomeViewModel(
|
|||||||
|
|
||||||
fun setViewList(list: SnapshotStateList<GameModel>) {
|
fun setViewList(list: SnapshotStateList<GameModel>) {
|
||||||
gameList = list
|
gameList = list
|
||||||
applyFilter()
|
reloadGameList(loadedCache.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLoadedCache(){
|
||||||
|
loadedCache = listOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
private var progress: MutableState<String>? = null
|
private var progress: MutableState<String>? = null
|
||||||
private var progressValue: MutableState<Float>? = null
|
private var progressValue: MutableState<Float>? = null
|
||||||
private var showLoading: MutableState<Boolean>? = null
|
private var showLoading: MutableState<Boolean>? = null
|
||||||
|
private var refreshUser: MutableState<Boolean>? = null
|
||||||
var gameHost: GameHost? = null
|
var gameHost: GameHost? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@ -168,6 +169,47 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearPptcCache(titleId :String){
|
||||||
|
if(titleId.isNotEmpty()){
|
||||||
|
val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu"
|
||||||
|
if(File(basePath).exists()){
|
||||||
|
var caches = mutableListOf<String>()
|
||||||
|
|
||||||
|
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<String>()
|
||||||
|
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(
|
fun setStatStates(
|
||||||
fifo: MutableState<Double>,
|
fifo: MutableState<Double>,
|
||||||
gameFps: MutableState<Double>,
|
gameFps: MutableState<Double>,
|
||||||
@ -213,4 +255,15 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
this.progress = progress
|
this.progress = progress
|
||||||
gameHost?.setProgressStates(showLoading, progressValue, progress)
|
gameHost?.setProgressStates(showLoading, progressValue, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setRefreshUserState(refreshUser: MutableState<Boolean>)
|
||||||
|
{
|
||||||
|
this.refreshUser = refreshUser
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestUserRefresh(){
|
||||||
|
refreshUser?.apply {
|
||||||
|
value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,27 +4,17 @@ import androidx.compose.runtime.MutableState
|
|||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.toLowerCase
|
import androidx.compose.ui.text.toLowerCase
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.anggrayudi.storage.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 com.google.gson.Gson
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import org.ryujinx.android.Helpers
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.LinkedList
|
|
||||||
import java.util.Queue
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class TitleUpdateViewModel(val titleId: String) {
|
class TitleUpdateViewModel(val titleId: String) {
|
||||||
|
private var canClose: MutableState<Boolean>? = null
|
||||||
private var basePath: String
|
private var basePath: String
|
||||||
private var updateJsonName = "updates.json"
|
private var updateJsonName = "updates.json"
|
||||||
private var stagingUpdateJsonName = "staging_updates.json"
|
|
||||||
private var storageHelper: SimpleStorageHelper
|
private var storageHelper: SimpleStorageHelper
|
||||||
var pathsState: SnapshotStateList<String>? = null
|
var pathsState: SnapshotStateList<String>? = null
|
||||||
|
|
||||||
@ -37,32 +27,37 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
data?.paths?.apply {
|
data?.paths?.apply {
|
||||||
removeAt(index - 1)
|
val removed = removeAt(index - 1)
|
||||||
|
File(removed).deleteRecursively()
|
||||||
pathsState?.clear()
|
pathsState?.clear()
|
||||||
pathsState?.addAll(this)
|
pathsState?.addAll(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Add() {
|
fun Add(
|
||||||
|
isCopying: MutableState<Boolean>,
|
||||||
|
copyProgress: MutableState<Float>,
|
||||||
|
currentProgressName: MutableState<String>
|
||||||
|
) {
|
||||||
val callBack = storageHelper.onFileSelected
|
val callBack = storageHelper.onFileSelected
|
||||||
|
|
||||||
storageHelper.onFileSelected = { requestCode, files ->
|
storageHelper.onFileSelected = { requestCode, files ->
|
||||||
run {
|
run {
|
||||||
storageHelper.onFileSelected = callBack
|
storageHelper.onFileSelected = callBack
|
||||||
if(requestCode == UpdateRequestCode)
|
if (requestCode == UpdateRequestCode) {
|
||||||
{
|
|
||||||
val file = files.firstOrNull()
|
val file = files.firstOrNull()
|
||||||
file?.apply {
|
file?.apply {
|
||||||
val path = file.getAbsolutePath(storageHelper.storage.context)
|
// Copy updates to internal data folder
|
||||||
if(path.isNotEmpty()){
|
val updatePath = "$basePath/update"
|
||||||
data?.apply {
|
File(updatePath).mkdirs()
|
||||||
if(!paths.contains(path)) {
|
Helpers.copyToData(
|
||||||
paths.add(path)
|
this,
|
||||||
pathsState?.clear()
|
updatePath,
|
||||||
pathsState?.addAll(paths)
|
storageHelper,
|
||||||
}
|
isCopying,
|
||||||
}
|
copyProgress,
|
||||||
}
|
currentProgressName, ::refreshPaths
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,68 +65,43 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
storageHelper.openFilePicker(UpdateRequestCode)
|
storageHelper.openFilePicker(UpdateRequestCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshPaths() {
|
||||||
|
data?.apply {
|
||||||
|
val updatePath = "$basePath/update"
|
||||||
|
val existingPaths = mutableListOf<String>()
|
||||||
|
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(
|
fun save(
|
||||||
index: Int,
|
index: Int,
|
||||||
isCopying: MutableState<Boolean>,
|
openDialog: MutableState<Boolean>
|
||||||
openDialog: MutableState<Boolean>,
|
|
||||||
copyProgress: MutableState<Float>,
|
|
||||||
currentProgressName: MutableState<String>
|
|
||||||
) {
|
) {
|
||||||
data?.apply {
|
data?.apply {
|
||||||
|
val updatePath = "$basePath/update"
|
||||||
this.selected = ""
|
this.selected = ""
|
||||||
if (paths.isNotEmpty() && index > 0) {
|
if (paths.isNotEmpty() && index > 0) {
|
||||||
val ind = max(index - 1, paths.count() - 1)
|
val ind = max(index - 1, paths.count() - 1)
|
||||||
this.selected = paths[ind]
|
this.selected = paths[ind]
|
||||||
}
|
}
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
var json = gson.toJson(this)
|
|
||||||
File(basePath).mkdirs()
|
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 metadata = TitleUpdateMetadata()
|
||||||
var queue: Queue<String> = LinkedList()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun finish() {
|
|
||||||
val savedUpdates = mutableListOf<String>()
|
val savedUpdates = mutableListOf<String>()
|
||||||
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
|
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
|
||||||
var missingFiles =
|
metadata.paths = savedUpdates
|
||||||
savedUpdates.filter { i -> paths.find { it.endsWith(File(i).name) } == null }
|
|
||||||
for (path in missingFiles) {
|
|
||||||
File(path).delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectedName = File(selected).name
|
val selectedName = File(selected).name
|
||||||
val newSelectedPath = "$updatePath/$selectedName"
|
val newSelectedPath = "$updatePath/$selectedName"
|
||||||
@ -139,59 +109,16 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
metadata.selected = newSelectedPath
|
metadata.selected = newSelectedPath
|
||||||
}
|
}
|
||||||
|
|
||||||
json = gson.toJson(metadata)
|
var json = gson.toJson(metadata)
|
||||||
File("$basePath/$updateJsonName").writeText(json)
|
File("$basePath/$updateJsonName").writeText(json)
|
||||||
|
|
||||||
openDialog.value = false
|
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPaths(paths: SnapshotStateList<String>) {
|
fun setPaths(paths: SnapshotStateList<String>, canClose: MutableState<Boolean>) {
|
||||||
pathsState = paths
|
pathsState = paths
|
||||||
|
this.canClose = canClose
|
||||||
data?.apply {
|
data?.apply {
|
||||||
pathsState?.clear()
|
pathsState?.clear()
|
||||||
pathsState?.addAll(this.paths)
|
pathsState?.addAll(this.paths)
|
||||||
@ -203,29 +130,14 @@ class TitleUpdateViewModel(val titleId: String) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
|
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
|
||||||
val stagingJson = "${basePath}/${stagingUpdateJsonName}"
|
|
||||||
jsonPath = "${basePath}/${updateJsonName}"
|
jsonPath = "${basePath}/${updateJsonName}"
|
||||||
|
|
||||||
data = TitleUpdateMetadata()
|
data = TitleUpdateMetadata()
|
||||||
if (File(stagingJson).exists()) {
|
if (File(jsonPath).exists()) {
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java)
|
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
|
||||||
|
|
||||||
data?.apply {
|
refreshPaths()
|
||||||
val existingPaths = mutableListOf<String>()
|
|
||||||
for (path in paths) {
|
|
||||||
if (File(path).exists()) {
|
|
||||||
existingPaths.add(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!existingPaths.contains(selected)){
|
|
||||||
selected = ""
|
|
||||||
}
|
|
||||||
pathsState?.clear()
|
|
||||||
pathsState?.addAll(existingPaths)
|
|
||||||
paths = existingPaths
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
storageHelper = MainActivity.StorageHelper!!
|
storageHelper = MainActivity.StorageHelper!!
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package org.ryujinx.android.views
|
package org.ryujinx.android.views
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.view.Gravity
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
@ -20,52 +22,51 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.BottomAppBar
|
||||||
|
import androidx.compose.material3.BottomAppBarDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.DockedSearchBar
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogWindowProvider
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.anggrayudi.storage.extension.launchOnUiThread
|
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||||
import org.ryujinx.android.MainActivity
|
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.GameModel
|
||||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Base64
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -76,27 +77,54 @@ class HomeViews {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainTopBar(
|
fun Home(
|
||||||
navController: NavHostController,
|
viewModel: HomeViewModel = HomeViewModel(),
|
||||||
query: MutableState<String>,
|
navController: NavHostController? = null
|
||||||
refresh: MutableState<Boolean>
|
|
||||||
) {
|
) {
|
||||||
val topBarSize = remember {
|
val native = RyujinxNative()
|
||||||
mutableStateOf(0)
|
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("")
|
||||||
}
|
}
|
||||||
Column {
|
val refresh = remember {
|
||||||
val showOptionsPopup = remember {
|
mutableStateOf(true)
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
}
|
||||||
|
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 = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.zIndex(1f)
|
.fillMaxWidth()
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp),
|
||||||
.onSizeChanged {
|
|
||||||
topBarSize.value = it.height
|
|
||||||
},
|
|
||||||
title = {
|
title = {
|
||||||
DockedSearchBar(
|
SearchBar(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = SearchBarDefaults.inputFieldShape,
|
shape = SearchBarDefaults.inputFieldShape,
|
||||||
query = query.value,
|
query = query.value,
|
||||||
onQueryChange = {
|
onQueryChange = {
|
||||||
@ -112,134 +140,147 @@ class HomeViews {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(text = "Search Games")
|
Text(text = "Ryujinx")
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
) { }
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(onClick = {
|
||||||
onClick = {
|
navController?.navigate("user")
|
||||||
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 =
|
if (pic.value.isNotEmpty()) {
|
||||||
LocalView.current.parent as DialogWindowProvider
|
Image(
|
||||||
dialogWindowProvider.window.setGravity(Gravity.TOP)
|
bitmap = BitmapFactory.decodeByteArray(
|
||||||
Surface(
|
pic.value,
|
||||||
|
0,
|
||||||
|
pic.value.size
|
||||||
|
)
|
||||||
|
.asImageBitmap(),
|
||||||
|
contentDescription = "user image",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.padding(4.dp)
|
||||||
.wrapContentHeight()
|
.size(52.dp)
|
||||||
.padding(16.dp),
|
.clip(CircleShape)
|
||||||
shape = MaterialTheme.shapes.large,
|
)
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
} else {
|
||||||
) {
|
Icon(
|
||||||
Column {
|
Icons.Filled.Person,
|
||||||
TextButton(
|
contentDescription = "user"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate("settings")
|
navController?.navigate("settings")
|
||||||
},
|
}
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.Start),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Settings,
|
Icons.Filled.Settings,
|
||||||
contentDescription = "Settings"
|
contentDescription = "Settings"
|
||||||
)
|
)
|
||||||
Text(
|
}
|
||||||
text = "Settings", modifier = Modifier
|
}
|
||||||
.padding(16.dp)
|
)
|
||||||
.align(Alignment.CenterVertically)
|
},
|
||||||
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) {
|
|
||||||
val sheetState = rememberModalBottomSheetState()
|
|
||||||
val showBottomSheet = remember { mutableStateOf(false) }
|
|
||||||
val showLoading = remember { mutableStateOf(false) }
|
|
||||||
val query = remember {
|
|
||||||
mutableStateOf("")
|
|
||||||
}
|
|
||||||
val refresh = remember {
|
|
||||||
mutableStateOf(true)
|
|
||||||
}
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
topBar = {
|
|
||||||
navController?.apply {
|
|
||||||
MainTopBar(navController, query, refresh)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(onClick = {
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
viewModel.openGameFolder()
|
viewModel.openGameFolder()
|
||||||
},
|
},
|
||||||
shape = CircleShape) {
|
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
|
||||||
|
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Add,
|
org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface),
|
||||||
contentDescription = "Options"
|
contentDescription = "Open Folder"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(modifier = Modifier.padding(contentPadding)) {
|
Box(modifier = Modifier.padding(contentPadding)) {
|
||||||
val list = remember {
|
val list = remember {
|
||||||
mutableStateListOf<GameModel>()
|
mutableStateListOf<GameModel>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (refresh.value) {
|
if (refresh.value) {
|
||||||
viewModel.setViewList(list)
|
viewModel.setViewList(list)
|
||||||
refresh.value = false
|
refresh.value = false
|
||||||
|
showAppActions.value = false
|
||||||
|
}
|
||||||
|
val selectedModel = remember {
|
||||||
|
mutableStateOf(viewModel.mainViewModel?.selected)
|
||||||
}
|
}
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
items(list) {
|
items(list) {
|
||||||
it.titleName?.apply {
|
it.titleName?.apply {
|
||||||
if (this.isNotEmpty() && (query.value.trim().isEmpty() || this.lowercase(
|
if (this.isNotEmpty() && (query.value.trim()
|
||||||
|
.isEmpty() || this.lowercase(
|
||||||
Locale.getDefault()
|
Locale.getDefault()
|
||||||
)
|
)
|
||||||
.contains(query.value)))
|
.contains(query.value))
|
||||||
GameItem(it, viewModel, showBottomSheet, showLoading)
|
)
|
||||||
|
GameItem(
|
||||||
|
it,
|
||||||
|
viewModel,
|
||||||
|
showAppActions,
|
||||||
|
showLoading,
|
||||||
|
selectedModel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,31 +288,29 @@ class HomeViews {
|
|||||||
|
|
||||||
if (showLoading.value) {
|
if (showLoading.value) {
|
||||||
AlertDialog(onDismissRequest = { }) {
|
AlertDialog(onDismissRequest = { }) {
|
||||||
Card(modifier = Modifier
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
shape = MaterialTheme.shapes.medium) {
|
shape = MaterialTheme.shapes.medium
|
||||||
Column(modifier = Modifier
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxWidth()) {
|
|
||||||
Text(text = "Loading")
|
|
||||||
LinearProgressIndicator(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
.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) {
|
if (openTitleUpdateDialog.value) {
|
||||||
AlertDialog(onDismissRequest = {
|
AlertDialog(onDismissRequest = {
|
||||||
openTitleUpdateDialog.value = false
|
openTitleUpdateDialog.value = false
|
||||||
@ -285,7 +324,7 @@ class HomeViews {
|
|||||||
) {
|
) {
|
||||||
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
|
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
|
||||||
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
|
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
|
||||||
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog)
|
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -308,59 +347,6 @@ class HomeViews {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,16 +355,31 @@ class HomeViews {
|
|||||||
fun GameItem(
|
fun GameItem(
|
||||||
gameModel: GameModel,
|
gameModel: GameModel,
|
||||||
viewModel: HomeViewModel,
|
viewModel: HomeViewModel,
|
||||||
showSheet: MutableState<Boolean>,
|
showAppActions: MutableState<Boolean>,
|
||||||
showLoading: MutableState<Boolean>
|
showLoading: MutableState<Boolean>,
|
||||||
|
selectedModel: MutableState<GameModel?>
|
||||||
) {
|
) {
|
||||||
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
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 {
|
thread {
|
||||||
showLoading.value = true
|
showLoading.value = true
|
||||||
val success =
|
val success =
|
||||||
@ -396,27 +397,32 @@ class HomeViews {
|
|||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
viewModel.mainViewModel?.selected = gameModel
|
viewModel.mainViewModel?.selected = gameModel
|
||||||
showSheet.value = true
|
showAppActions.value = true
|
||||||
})) {
|
selectedModel.value = gameModel
|
||||||
Row(modifier = Modifier
|
})
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000")
|
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
||||||
{
|
val iconSource =
|
||||||
val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
|
MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
|
||||||
val imageFile = File(iconSource)
|
val imageFile = File(iconSource)
|
||||||
if (imageFile.exists()) {
|
if (imageFile.exists()) {
|
||||||
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
val size = ImageSize / Resources.getSystem().displayMetrics.density
|
||||||
AsyncImage(model = imageFile,
|
AsyncImage(
|
||||||
|
model = imageFile,
|
||||||
contentDescription = gameModel.titleName + " icon",
|
contentDescription = gameModel.titleName + " icon",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 8.dp)
|
.padding(end = 8.dp)
|
||||||
.width(size.roundToInt().dp)
|
.width(size.roundToInt().dp)
|
||||||
.height(size.roundToInt().dp))
|
.height(size.roundToInt().dp)
|
||||||
}
|
)
|
||||||
else NotAvailableIcon()
|
} else NotAvailableIcon()
|
||||||
} else NotAvailableIcon()
|
} else NotAvailableIcon()
|
||||||
Column {
|
Column {
|
||||||
Text(text = gameModel.titleName ?: "")
|
Text(text = gameModel.titleName ?: "")
|
||||||
|
@ -16,12 +16,13 @@ class MainView {
|
|||||||
|
|
||||||
NavHost(navController = navController, startDestination = "home") {
|
NavHost(navController = navController, startDestination = "home") {
|
||||||
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
|
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
|
||||||
|
composable("user") { UserViews.Main(mainViewModel, navController) }
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
SettingViews.Main(
|
SettingViews.Main(
|
||||||
SettingsViewModel(
|
SettingsViewModel(
|
||||||
navController,
|
navController,
|
||||||
mainViewModel.activity
|
mainViewModel.activity
|
||||||
)
|
), mainViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ package org.ryujinx.android.views
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
import androidx.compose.animation.core.animateDp
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
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.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@ -51,16 +52,23 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import 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.SettingsViewModel
|
||||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class SettingViews {
|
class SettingViews {
|
||||||
companion object {
|
companion object {
|
||||||
const val EXPANSTION_TRANSITION_DURATION = 450
|
const val EXPANSTION_TRANSITION_DURATION = 450
|
||||||
|
const val IMPORT_CODE = 12341
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(settingsViewModel: SettingsViewModel) {
|
fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) {
|
||||||
val loaded = remember {
|
val loaded = remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@ -134,7 +142,9 @@ class SettingViews {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}) { contentPadding ->
|
}) { contentPadding ->
|
||||||
Column(modifier = Modifier.padding(contentPadding)) {
|
Column(modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.verticalScroll(rememberScrollState())) {
|
||||||
ExpandableView(onCardArrowClick = { }, title = "System") {
|
ExpandableView(onCardArrowClick = { }, title = "System") {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
@ -227,6 +237,121 @@ class SettingViews {
|
|||||||
ignoreMissingServices.value = !ignoreMissingServices.value
|
ignoreMissingServices.value = !ignoreMissingServices.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
val isImporting = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
val showImportWarning = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
val showImportCompletion = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
var importFile = remember {
|
||||||
|
mutableStateOf<DocumentFile?>(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") {
|
ExpandableView(onCardArrowClick = { }, title = "Graphics") {
|
||||||
@ -276,8 +401,11 @@ class SettingViews {
|
|||||||
text = "Enable Texture Recompression",
|
text = "Enable Texture Recompression",
|
||||||
modifier = Modifier.align(Alignment.CenterVertically)
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
)
|
)
|
||||||
Switch(checked = enableTextureRecompression.value, onCheckedChange = {
|
Switch(
|
||||||
enableTextureRecompression.value = !enableTextureRecompression.value
|
checked = enableTextureRecompression.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
enableTextureRecompression.value =
|
||||||
|
!enableTextureRecompression.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
@ -290,7 +418,8 @@ class SettingViews {
|
|||||||
var isDriverSelectorOpen = remember {
|
var isDriverSelectorOpen = remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
var driverViewModel = VulkanDriverViewModel(settingsViewModel.activity)
|
var driverViewModel =
|
||||||
|
VulkanDriverViewModel(settingsViewModel.activity)
|
||||||
var isChanged = remember {
|
var isChanged = remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@ -329,11 +458,15 @@ class SettingViews {
|
|||||||
isChanged.value = true
|
isChanged.value = true
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
Column (modifier = Modifier
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(300.dp)) {
|
.height(300.dp)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
@ -359,7 +492,9 @@ class SettingViews {
|
|||||||
for (driver in drivers) {
|
for (driver in drivers) {
|
||||||
var ind = driverIndex
|
var ind = driverIndex
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
@ -378,15 +513,21 @@ class SettingViews {
|
|||||||
driverViewModel.selected =
|
driverViewModel.selected =
|
||||||
driver.driverPath
|
driver.driverPath
|
||||||
}) {
|
}) {
|
||||||
Text(text = driver.libraryName,
|
Text(
|
||||||
|
text = driver.libraryName,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth())
|
.fillMaxWidth()
|
||||||
Text(text = driver.driverVersion,
|
)
|
||||||
|
Text(
|
||||||
|
text = driver.driverVersion,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth())
|
.fillMaxWidth()
|
||||||
Text(text = driver.description,
|
)
|
||||||
|
Text(
|
||||||
|
text = driver.description,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth())
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,24 +626,6 @@ class SettingViews {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val transition = updateTransition(transitionState, label = "transition")
|
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({
|
val arrowRotationDegree by transition.animateFloat({
|
||||||
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
|
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
|
||||||
}, label = "rotationDegreeTransition") {
|
}, label = "rotationDegreeTransition") {
|
||||||
@ -514,7 +637,7 @@ class SettingViews {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = cardPaddingHorizontal,
|
horizontal = 24.dp,
|
||||||
vertical = 8.dp
|
vertical = 8.dp
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -27,11 +27,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class TitleUpdateViews {
|
class TitleUpdateViews {
|
||||||
companion object {
|
companion object {
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
|
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
|
||||||
val viewModel = TitleUpdateViewModel(titleId)
|
val viewModel = TitleUpdateViewModel(titleId)
|
||||||
|
|
||||||
val selected = remember { mutableStateOf(0) }
|
val selected = remember { mutableStateOf(0) }
|
||||||
@ -46,6 +47,9 @@ class TitleUpdateViews {
|
|||||||
val copyProgress = remember {
|
val copyProgress = remember {
|
||||||
mutableStateOf(0.0f)
|
mutableStateOf(0.0f)
|
||||||
}
|
}
|
||||||
|
var currentProgressName = remember {
|
||||||
|
mutableStateOf("Starting Copy")
|
||||||
|
}
|
||||||
Column {
|
Column {
|
||||||
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
|
||||||
Surface(
|
Surface(
|
||||||
@ -77,7 +81,7 @@ class TitleUpdateViews {
|
|||||||
mutableStateListOf<String>()
|
mutableStateListOf<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.setPaths(paths)
|
viewModel.setPaths(paths, canClose)
|
||||||
var index = 1
|
var index = 1
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
val i = index
|
val i = index
|
||||||
@ -86,7 +90,7 @@ class TitleUpdateViews {
|
|||||||
selected = (selected.value == i),
|
selected = (selected.value == i),
|
||||||
onClick = { selected.value = i })
|
onClick = { selected.value = i })
|
||||||
Text(
|
Text(
|
||||||
text = path,
|
text = File(path).name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
@ -111,7 +115,7 @@ class TitleUpdateViews {
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.Add()
|
viewModel.Add(isCopying, copyProgress, currentProgressName)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -122,22 +126,33 @@ class TitleUpdateViews {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
var currentProgressName = remember {
|
|
||||||
mutableStateOf("Starting Copy")
|
|
||||||
}
|
|
||||||
if (isCopying.value) {
|
if (isCopying.value) {
|
||||||
Text(text = "Copying updates to local storage")
|
Text(text = "Copying updates to local storage")
|
||||||
Text(text = currentProgressName.value)
|
Text(text = currentProgressName.value)
|
||||||
|
Row {
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
progress = copyProgress.value
|
progress = copyProgress.value
|
||||||
)
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
isCopying.value = false
|
||||||
|
canClose.value = true
|
||||||
|
viewModel.refreshPaths()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName)
|
if (!isCopying.value) {
|
||||||
|
canClose.value = true
|
||||||
|
viewModel.save(selected.value, openDialog)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
@ -0,0 +1,35 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="255.76"
|
||||||
|
android:viewportHeight="255.76">
|
||||||
|
<group android:scaleX="0.44"
|
||||||
|
android:scaleY="0.44"
|
||||||
|
android:translateX="71.6128"
|
||||||
|
android:translateY="71.6128">
|
||||||
|
<path
|
||||||
|
android:pathData="M80.63,0V220.39H44.37c-14,0 -35.74,-20.74 -35.74,-39.13V40.13C8.63,19.19 31.36,0 49.06,0Z"
|
||||||
|
android:fillColor="#02c5e5"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M175.13,35.37V255.76h36.26c14,0 35.74,-20.74 35.74,-39.13V75.5c0,-20.94 -22.73,-40.13 -40.43,-40.13Z"
|
||||||
|
android:fillColor="#ff5f55"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M124.34,137.96l-1.76,7.61l-31.94,0l2.25,-7.61l31.45,0z"
|
||||||
|
android:fillColor="#02c5e5"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M160.29,137.96l-2.45,7.61l-35.26,0l1.76,-7.61l35.95,0z"
|
||||||
|
android:fillColor="#ff5f55"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M130.39,111.86l-1.77,7.61l-33.48,0l2.25,-7.61l33,0z"
|
||||||
|
android:fillColor="#02c5e5"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M164.79,111.86l-2.45,7.61l-33.72,0l1.77,-7.61l34.4,0z"
|
||||||
|
android:fillColor="#ff5f55"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M104.24,167.99l18.59,-80.22l6.95,0l-18.59,80.22l-6.95,0z"
|
||||||
|
android:fillColor="#02c5e5"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M128.18,167.99l18.59,-80.22l7.12,0l-18.59,80.22l-7.12,0z"
|
||||||
|
android:fillColor="#ff5f55"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.3 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|