Archived
1
0
forked from MeloNX/MeloNX

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
This commit is contained in:
Emmanuel Hansen 2023-10-28 15:23:39 +00:00
parent 898f88350c
commit a583d6bf46
38 changed files with 1333 additions and 715 deletions

View File

@ -18,6 +18,7 @@ using Silk.NET.Vulkan.Extensions.KHR;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
@ -40,6 +41,9 @@ namespace LibRyujinx
[DllImport("libryujinxjni")]
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
[DllImport("libryujinxjni")]
private extern static void pushString(string ch);
[DllImport("libryujinxjni")]
internal extern static void setRenderingThread();
@ -95,6 +99,12 @@ namespace LibRyujinx
return s;
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceReloadFilesystem")]
public static void JniReloadFileSystem()
{
SwitchDevice?.ReloadFileSystem();
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInitialize")]
public static JBoolean JniInitializeDeviceNative(JEnvRef jEnv,
JObjectLocalRef jObj,
@ -503,6 +513,89 @@ namespace LibRyujinx
return ConnectGamepad(index);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetOpenedUser")]
public static void JniGetOpenedUser(JEnvRef jEnv, JObjectLocalRef jObj)
{
var userId = GetOpenedUser();
pushString(userId);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserPicture")]
public static JStringLocalRef JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
return CreateString(jEnv, GetUserPicture(userId));
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserPicture")]
public static void JniGetUserPicture(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef picturePtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
var picture = GetString(jEnv, picturePtr) ?? "";
SetUserPicture(userId, picture);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetUserName")]
public static JStringLocalRef JniGetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
return CreateString(jEnv, GetUserName(userId));
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userSetUserName")]
public static void JniSetUserName(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr, JStringLocalRef userNamePtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
var userName = GetString(jEnv, userNamePtr) ?? "";
SetUserName(userId, userName);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userGetAllUsers")]
public static JArrayLocalRef JniGetAllUsers(JEnvRef jEnv, JObjectLocalRef jObj)
{
var users = GetAllUsers();
return CreateStringArray(jEnv, users.ToList());
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userAddUser")]
public static void JniAddUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userNamePtr, JStringLocalRef picturePtr)
{
var userName = GetString(jEnv, userNamePtr) ?? "";
var picture = GetString(jEnv, picturePtr) ?? "";
AddUser(userName, picture);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userDeleteUser")]
public static void JniDeleteUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
DeleteUser(userId);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")]
public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
OpenUser(userId);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userCloseUser")]
public static void JniCloseUser(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef userIdPtr)
{
var userId = GetString(jEnv, userIdPtr) ?? "";
CloseUser(userId);
}
private static FileStream OpenFile(int descriptor)
{
var safeHandle = new SafeFileHandle(descriptor, false);

View File

@ -11,8 +11,8 @@ android {
applicationId "org.ryujinx.android"
minSdk 30
targetSdk 33
versionCode 1
versionName "1.0"
versionCode 10001
versionName '1.0.1'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -99,6 +99,7 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.2.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0")
implementation "io.coil-kt:coil-compose:2.4.0"
testImplementation 'junit:junit:4.13.2'

View File

@ -24,7 +24,6 @@
android:icon="@mipmap/ic_launcher"
android:isGame="true"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RyujinxAndroid"
tools:targetApi="31">

View File

@ -45,5 +45,6 @@ long _currentRenderingThreadId = 0;
JavaVM* _vm = nullptr;
jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr;
std::string _currentString = "";
#endif //RYUJINXNATIVE_RYUIJNX_H

View File

@ -311,3 +311,25 @@ JNIEXPORT jstring JNICALL
Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
return createStringFromStdString(env, progressInfo);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_org_ryujinx_android_NativeHelpers_popStringJava(JNIEnv *env, jobject thiz) {
return createStringFromStdString(env, _currentString);
}
extern "C"
JNIEXPORT void JNICALL
Java_org_ryujinx_android_NativeHelpers_pushStringJava(JNIEnv *env, jobject thiz, jstring string) {
_currentString = getStringPointer(env, string);
}
extern "C"
void pushString(char* str){
_currentString = str;
}
extern "C"
const char* popString(){
return _currentString.c_str();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

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

View File

@ -6,7 +6,6 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
@ -51,7 +50,7 @@ import org.ryujinx.android.viewmodels.QuickSettings
import kotlin.math.abs
import kotlin.math.roundToInt
class GameActivity : ComponentActivity() {
class GameActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager =
PhysicalControllerManager(this)
@ -355,6 +354,7 @@ class GameActivity : ComponentActivity() {
.padding(16.dp)
) {
Button(onClick = {
showBackNotice.value = false
mainViewModel.closeGame()
setFullScreen(false)
finishActivity(0)

View File

@ -7,9 +7,22 @@ import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.copyFileTo
import com.anggrayudi.storage.file.openInputStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.lingala.zip4j.io.inputstream.ZipInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
class Helpers {
companion object{
companion object {
fun getPath(context: Context, uri: Uri): String? {
// DocumentProvider
@ -25,7 +38,10 @@ class Helpers {
} else if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
@ -36,9 +52,11 @@ class Helpers {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
@ -54,13 +72,81 @@ class Helpers {
}
return null
}
fun copyToData(
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
isCopying: MutableState<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
val column = "_data"
val projection = arrayOf(column)
try {
cursor = uri?.let { context.contentResolver.query(it, projection, selection, selectionArgs,null) }
cursor = uri?.let {
context.contentResolver.query(
it,
projection,
selection,
selectionArgs,
null
)
}
if (cursor != null && cursor.moveToFirst()) {
val column_index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index)
@ -82,5 +168,56 @@ class Helpers {
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
fun importAppData(
file: DocumentFile,
isImporting: MutableState<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()
}
}
}
}

View File

@ -23,6 +23,189 @@ class Icons {
companion object{
/// Icons exported from https://www.composables.com/icons
@Composable
fun playArrow(color: Color): ImageVector {
return remember {
ImageVector.Builder(
name = "play_arrow",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(color),
fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(15.542f, 30f)
quadToRelative(-0.667f, 0.458f, -1.334f, 0.062f)
quadToRelative(-0.666f, -0.395f, -0.666f, -1.187f)
verticalLineTo(10.917f)
quadToRelative(0f, -0.75f, 0.666f, -1.146f)
quadToRelative(0.667f, -0.396f, 1.334f, 0.062f)
lineToRelative(14.083f, 9f)
quadToRelative(0.583f, 0.375f, 0.583f, 1.084f)
quadToRelative(0f, 0.708f, -0.583f, 1.083f)
close()
moveToRelative(0.625f, -10.083f)
close()
moveToRelative(0f, 6.541f)
lineToRelative(10.291f, -6.541f)
lineToRelative(-10.291f, -6.542f)
close()
}
}.build()
}
}
@Composable
fun folderOpen(color: Color): ImageVector {
return remember {
ImageVector.Builder(
name = "folder_open",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(color),
fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(6.25f, 33.125f)
quadToRelative(-1.083f, 0f, -1.854f, -0.792f)
quadToRelative(-0.771f, -0.791f, -0.771f, -1.875f)
verticalLineTo(9.667f)
quadToRelative(0f, -1.084f, 0.771f, -1.854f)
quadToRelative(0.771f, -0.771f, 1.854f, -0.771f)
horizontalLineToRelative(10.042f)
quadToRelative(0.541f, 0f, 1.041f, 0.208f)
quadToRelative(0.5f, 0.208f, 0.834f, 0.583f)
lineToRelative(1.875f, 1.834f)
horizontalLineTo(33.75f)
quadToRelative(1.083f, 0f, 1.854f, 0.791f)
quadToRelative(0.771f, 0.792f, 0.771f, 1.834f)
horizontalLineTo(18.917f)
lineTo(16.25f, 9.667f)
horizontalLineToRelative(-10f)
verticalLineTo(30.25f)
lineToRelative(3.542f, -13.375f)
quadToRelative(0.25f, -0.875f, 0.979f, -1.396f)
quadToRelative(0.729f, -0.521f, 1.604f, -0.521f)
horizontalLineToRelative(23.25f)
quadToRelative(1.292f, 0f, 2.104f, 1.021f)
quadToRelative(0.813f, 1.021f, 0.438f, 2.271f)
lineToRelative(-3.459f, 12.833f)
quadToRelative(-0.291f, 1f, -1f, 1.521f)
quadToRelative(-0.708f, 0.521f, -1.75f, 0.521f)
close()
moveToRelative(2.708f, -2.667f)
horizontalLineToRelative(23.167f)
lineToRelative(3.417f, -12.875f)
horizontalLineTo(12.333f)
close()
moveToRelative(0f, 0f)
lineToRelative(3.375f, -12.875f)
lineToRelative(-3.375f, 12.875f)
close()
moveToRelative(-2.708f, -15.5f)
verticalLineTo(9.667f)
verticalLineToRelative(5.291f)
close()
}
}.build()
}
}
@Composable
fun gameUpdate(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {
ImageVector.Builder(
name = "game_update_alt",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(Color.Black.copy(alpha = 0.5f)),
stroke = SolidColor(primaryColor),
fillAlpha = 1f,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(6.25f, 33.083f)
quadToRelative(-1.083f, 0f, -1.854f, -0.791f)
quadToRelative(-0.771f, -0.792f, -0.771f, -1.834f)
verticalLineTo(9.542f)
quadToRelative(0f, -1.042f, 0.771f, -1.854f)
quadToRelative(0.771f, -0.813f, 1.854f, -0.813f)
horizontalLineToRelative(8.458f)
quadToRelative(0.584f, 0f, 0.959f, 0.396f)
reflectiveQuadToRelative(0.375f, 0.937f)
quadToRelative(0f, 0.584f, -0.375f, 0.959f)
reflectiveQuadToRelative(-0.959f, 0.375f)
horizontalLineTo(6.25f)
verticalLineToRelative(20.916f)
horizontalLineToRelative(27.542f)
verticalLineTo(9.542f)
horizontalLineToRelative(-8.5f)
quadToRelative(-0.584f, 0f, -0.959f, -0.375f)
reflectiveQuadToRelative(-0.375f, -0.959f)
quadToRelative(0f, -0.541f, 0.375f, -0.937f)
reflectiveQuadToRelative(0.959f, -0.396f)
horizontalLineToRelative(8.5f)
quadToRelative(1.041f, 0f, 1.833f, 0.813f)
quadToRelative(0.792f, 0.812f, 0.792f, 1.854f)
verticalLineToRelative(20.916f)
quadToRelative(0f, 1.042f, -0.792f, 1.834f)
quadToRelative(-0.792f, 0.791f, -1.833f, 0.791f)
close()
moveTo(20f, 25f)
quadToRelative(-0.25f, 0f, -0.479f, -0.083f)
quadToRelative(-0.229f, -0.084f, -0.396f, -0.292f)
lineTo(12.75f, 18.25f)
quadToRelative(-0.375f, -0.333f, -0.375f, -0.896f)
quadToRelative(0f, -0.562f, 0.417f, -0.979f)
quadToRelative(0.375f, -0.375f, 0.916f, -0.375f)
quadToRelative(0.542f, 0f, 0.959f, 0.375f)
lineToRelative(4.041f, 4.083f)
verticalLineTo(8.208f)
quadToRelative(0f, -0.541f, 0.375f, -0.937f)
reflectiveQuadTo(20f, 6.875f)
quadToRelative(0.542f, 0f, 0.938f, 0.396f)
quadToRelative(0.395f, 0.396f, 0.395f, 0.937f)
verticalLineToRelative(12.25f)
lineToRelative(4.084f, -4.083f)
quadToRelative(0.333f, -0.333f, 0.875f, -0.333f)
quadToRelative(0.541f, 0f, 0.916f, 0.375f)
quadToRelative(0.417f, 0.416f, 0.417f, 0.958f)
reflectiveQuadToRelative(-0.375f, 0.917f)
lineToRelative(-6.333f, 6.333f)
quadToRelative(-0.209f, 0.208f, -0.438f, 0.292f)
quadTo(20.25f, 25f, 20f, 25f)
close()
}
}.build()
}
}
@Composable
fun download(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary
return remember {

View File

@ -4,7 +4,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
@ -17,7 +16,7 @@ import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.views.MainView
class MainActivity : ComponentActivity() {
class MainActivity : BaseActivity() {
private var _isInit: Boolean = false
var storageHelper: SimpleStorageHelper? = null
companion object {

View File

@ -28,4 +28,6 @@ class NativeHelpers {
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
external fun getProgressInfo() : String
external fun getProgressValue() : Float
external fun pushStringJava(string: String)
external fun popStringJava() : String
}

View File

@ -38,6 +38,7 @@ class RyujinxNative {
external fun graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop()
external fun deviceReloadFilesystem()
external fun inputInitialize(width: Int, height: Int)
external fun inputSetClientSize(width: Int, height: Int)
external fun inputSetTouchPoint(x: Int, y: Int)
@ -52,4 +53,14 @@ class RyujinxNative {
external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String
external fun deviceGetDlcContentList(path: String, titleId: Long) : Array<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)
}

View File

@ -68,7 +68,7 @@ class HomeViewModel(
)
}
fun reloadGameList() {
fun reloadGameList(ignoreCache: Boolean = false) {
var storage = activity?.storageHelper ?: return
if(isLoading)
@ -77,27 +77,32 @@ class HomeViewModel(
isLoading = true
val files = mutableListOf<GameModel>()
if(!ignoreCache) {
val files = mutableListOf<GameModel>()
thread {
try {
for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp")
activity.let {
files.add(GameModel(file, it))
}
thread {
try {
for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp")
activity.let {
files.add(GameModel(file, it))
}
}
loadedCache = files.toList()
isLoading = false
applyFilter()
} finally {
isLoading = false
}
loadedCache = files.toList()
isLoading = false
applyFilter()
}
finally {
isLoading = false
}
}
else{
isLoading = false
applyFilter()
}
}
private fun applyFilter() {
@ -109,6 +114,10 @@ class HomeViewModel(
fun setViewList(list: SnapshotStateList<GameModel>) {
gameList = list
applyFilter()
reloadGameList(loadedCache.isNotEmpty())
}
fun clearLoadedCache(){
loadedCache = listOf()
}
}

View File

@ -37,6 +37,7 @@ class MainViewModel(val activity: MainActivity) {
private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = null
private var showLoading: MutableState<Boolean>? = null
private var refreshUser: MutableState<Boolean>? = null
var gameHost: GameHost? = null
set(value) {
field = value
@ -168,6 +169,47 @@ class MainViewModel(val activity: MainActivity) {
return true
}
fun clearPptcCache(titleId :String){
if(titleId.isNotEmpty()){
val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu"
if(File(basePath).exists()){
var caches = mutableListOf<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(
fifo: MutableState<Double>,
gameFps: MutableState<Double>,
@ -213,4 +255,15 @@ class MainViewModel(val activity: MainActivity) {
this.progress = progress
gameHost?.setProgressStates(showLoading, progressValue, progress)
}
fun setRefreshUserState(refreshUser: MutableState<Boolean>)
{
this.refreshUser = refreshUser
}
fun requestUserRefresh(){
refreshUser?.apply {
value = true
}
}
}

View File

@ -4,27 +4,17 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileType
import com.anggrayudi.storage.file.copyFileTo
import com.anggrayudi.storage.file.getAbsolutePath
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity
import java.io.File
import java.util.LinkedList
import java.util.Queue
import kotlin.math.max
class TitleUpdateViewModel(val titleId: String) {
private var canClose: MutableState<Boolean>? = null
private var basePath: String
private var updateJsonName = "updates.json"
private var stagingUpdateJsonName = "staging_updates.json"
private var storageHelper: SimpleStorageHelper
var pathsState: SnapshotStateList<String>? = null
@ -37,32 +27,37 @@ class TitleUpdateViewModel(val titleId: String) {
return
data?.paths?.apply {
removeAt(index - 1)
val removed = removeAt(index - 1)
File(removed).deleteRecursively()
pathsState?.clear()
pathsState?.addAll(this)
}
}
fun Add() {
fun Add(
isCopying: MutableState<Boolean>,
copyProgress: MutableState<Float>,
currentProgressName: MutableState<String>
) {
val callBack = storageHelper.onFileSelected
storageHelper.onFileSelected = { requestCode, files ->
run {
storageHelper.onFileSelected = callBack
if(requestCode == UpdateRequestCode)
{
if (requestCode == UpdateRequestCode) {
val file = files.firstOrNull()
file?.apply {
val path = file.getAbsolutePath(storageHelper.storage.context)
if(path.isNotEmpty()){
data?.apply {
if(!paths.contains(path)) {
paths.add(path)
pathsState?.clear()
pathsState?.addAll(paths)
}
}
}
// Copy updates to internal data folder
val updatePath = "$basePath/update"
File(updatePath).mkdirs()
Helpers.copyToData(
this,
updatePath,
storageHelper,
isCopying,
copyProgress,
currentProgressName, ::refreshPaths
)
}
}
}
@ -70,128 +65,60 @@ class TitleUpdateViewModel(val titleId: String) {
storageHelper.openFilePicker(UpdateRequestCode)
}
fun refreshPaths() {
data?.apply {
val updatePath = "$basePath/update"
val existingPaths = mutableListOf<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(
index: Int,
isCopying: MutableState<Boolean>,
openDialog: MutableState<Boolean>,
copyProgress: MutableState<Float>,
currentProgressName: MutableState<String>
openDialog: MutableState<Boolean>
) {
data?.apply {
val updatePath = "$basePath/update"
this.selected = ""
if (paths.isNotEmpty() && index > 0) {
val ind = max(index - 1, paths.count() - 1)
this.selected = paths[ind]
}
val gson = Gson()
var json = gson.toJson(this)
File(basePath).mkdirs()
File("$basePath/$stagingUpdateJsonName").writeText(json)
// Copy updates to internal data folder
val updatePath = "$basePath/update"
File(updatePath).mkdirs()
val ioScope = CoroutineScope(Dispatchers.IO)
var metadata = TitleUpdateMetadata()
var queue: Queue<String> = LinkedList()
val savedUpdates = mutableListOf<String>()
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
metadata.paths = savedUpdates
var callback: FileCallback? = null
fun copy(path: String) {
isCopying.value = true
val documentFile = DocumentFileCompat.fromFullPath(
storageHelper.storage.context,
path,
DocumentFileType.FILE
)
documentFile?.apply {
val stagedPath = "$basePath/${name}"
if (!File(stagedPath).exists()) {
var file = this
ioScope.launch {
file.copyFileTo(
storageHelper.storage.context,
File(updatePath),
callback = callback!!
)
}
metadata.paths.add(stagedPath)
}
}
val selectedName = File(selected).name
val newSelectedPath = "$updatePath/$selectedName"
if (File(newSelectedPath).exists()) {
metadata.selected = newSelectedPath
}
fun finish() {
val savedUpdates = mutableListOf<String>()
File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) }
var missingFiles =
savedUpdates.filter { i -> paths.find { it.endsWith(File(i).name) } == null }
for (path in missingFiles) {
File(path).delete()
}
var json = gson.toJson(metadata)
File("$basePath/$updateJsonName").writeText(json)
val selectedName = File(selected).name
val newSelectedPath = "$updatePath/$selectedName"
if (File(newSelectedPath).exists()) {
metadata.selected = newSelectedPath
}
json = gson.toJson(metadata)
File("$basePath/$updateJsonName").writeText(json)
openDialog.value = false
isCopying.value = false
}
callback = object : FileCallback() {
override fun onFailed(errorCode: FileCallback.ErrorCode) {
super.onFailed(errorCode)
}
override fun onStart(file: Any, workerThread: Thread): Long {
copyProgress.value = 0f
(file as DocumentFile)?.apply {
currentProgressName.value = "Copying ${file.name}"
}
return super.onStart(file, workerThread)
}
override fun onReport(report: Report) {
super.onReport(report)
copyProgress.value = report.progress / 100f
}
override fun onCompleted(result: Any) {
super.onCompleted(result)
if (queue.isNotEmpty())
copy(queue.remove())
else {
finish()
}
}
}
for (path in paths) {
queue.add(path)
}
ioScope.launch {
if (queue.isNotEmpty()) {
copy(queue.remove())
} else {
finish()
}
}
openDialog.value = false
}
}
fun setPaths(paths: SnapshotStateList<String>) {
fun setPaths(paths: SnapshotStateList<String>, canClose: MutableState<Boolean>) {
pathsState = paths
this.canClose = canClose
data?.apply {
pathsState?.clear()
pathsState?.addAll(this.paths)
@ -203,29 +130,14 @@ class TitleUpdateViewModel(val titleId: String) {
init {
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
val stagingJson = "${basePath}/${stagingUpdateJsonName}"
jsonPath = "${basePath}/${updateJsonName}"
data = TitleUpdateMetadata()
if (File(stagingJson).exists()) {
if (File(jsonPath).exists()) {
val gson = Gson()
data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java)
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
data?.apply {
val existingPaths = mutableListOf<String>()
for (path in paths) {
if (File(path).exists()) {
existingPaths.add(path)
}
}
if(!existingPaths.contains(selected)){
selected = ""
}
pathsState?.clear()
pathsState?.addAll(existingPaths)
paths = existingPaths
}
refreshPaths()
}
storageHelper = MainActivity.StorageHelper!!

View File

@ -1,8 +1,9 @@
package org.ryujinx.android.views
import android.content.res.Resources
import android.view.Gravity
import android.graphics.BitmapFactory
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
@ -20,52 +22,51 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread
import org.ryujinx.android.MainActivity
import org.ryujinx.android.R
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative
import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel
import java.io.File
import java.util.Base64
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.math.roundToInt
@ -76,289 +77,274 @@ class HomeViews {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTopBar(
navController: NavHostController,
query: MutableState<String>,
refresh: MutableState<Boolean>
fun Home(
viewModel: HomeViewModel = HomeViewModel(),
navController: NavHostController? = null
) {
val topBarSize = remember {
mutableStateOf(0)
}
Column {
val showOptionsPopup = remember {
mutableStateOf(false)
}
TopAppBar(
modifier = Modifier
.zIndex(1f)
.padding(top = 8.dp)
.onSizeChanged {
topBarSize.value = it.height
},
title = {
DockedSearchBar(
shape = SearchBarDefaults.inputFieldShape,
query = query.value,
onQueryChange = {
query.value = it
},
onSearch = {},
active = false,
onActiveChange = {},
leadingIcon = {
Icon(
Icons.Filled.Search,
contentDescription = "Search Games"
)
},
placeholder = {
Text(text = "Search Games")
}
) {
}
},
actions = {
IconButton(
onClick = {
refresh.value = true
}
) {
Icon(
Icons.Filled.Refresh,
contentDescription = "Refresh"
)
}
IconButton(
onClick = {
showOptionsPopup.value = true
}
) {
Icon(
Icons.Filled.MoreVert,
contentDescription = "More"
)
}
}
)
Box {
if (showOptionsPopup.value) {
AlertDialog(
modifier = Modifier.padding(
top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp,
start = 16.dp, end = 16.dp
),
onDismissRequest = {
showOptionsPopup.value = false
}) {
val dialogWindowProvider =
LocalView.current.parent as DialogWindowProvider
dialogWindowProvider.window.setGravity(Gravity.TOP)
Surface(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(16.dp),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column {
TextButton(
onClick = {
navController.navigate("settings")
},
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Start),
) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings"
)
Text(
text = "Settings", modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterVertically)
)
}
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) {
val sheetState = rememberModalBottomSheetState()
val showBottomSheet = remember { mutableStateOf(false) }
val native = RyujinxNative()
val showAppActions = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) }
val openTitleUpdateDialog = remember { mutableStateOf(false) }
val canClose = remember { mutableStateOf(true) }
val openDlcDialog = remember { mutableStateOf(false) }
val query = remember {
mutableStateOf("")
}
val refresh = remember {
mutableStateOf(true)
}
val refreshUser = remember {
mutableStateOf(true)
}
viewModel.mainViewModel?.setRefreshUserState(refreshUser)
val user = remember {
mutableStateOf("")
}
val pic = remember {
mutableStateOf(ByteArray(0))
}
if (refreshUser.value) {
native.userGetOpenedUser()
user.value = NativeHelpers().popStringJava()
if (user.value.isNotEmpty()) {
val decoder = Base64.getDecoder()
pic.value = decoder.decode(native.userGetUserPicture(user.value))
}
refreshUser.value = false;
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
navController?.apply {
MainTopBar(navController, query, refresh)
}
TopAppBar(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
title = {
SearchBar(
modifier = Modifier.fillMaxWidth(),
shape = SearchBarDefaults.inputFieldShape,
query = query.value,
onQueryChange = {
query.value = it
},
onSearch = {},
active = false,
onActiveChange = {},
leadingIcon = {
Icon(
Icons.Filled.Search,
contentDescription = "Search Games"
)
},
placeholder = {
Text(text = "Ryujinx")
}
) { }
},
actions = {
IconButton(onClick = {
navController?.navigate("user")
}) {
if (pic.value.isNotEmpty()) {
Image(
bitmap = BitmapFactory.decodeByteArray(
pic.value,
0,
pic.value.size
)
.asImageBitmap(),
contentDescription = "user image",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(4.dp)
.size(52.dp)
.clip(CircleShape)
)
} else {
Icon(
Icons.Filled.Person,
contentDescription = "user"
)
}
}
IconButton(
onClick = {
navController?.navigate("settings")
}
) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings"
)
}
}
)
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.openGameFolder()
bottomBar = {
BottomAppBar(actions = {
if (showAppActions.value) {
IconButton(onClick = {
}) {
Icon(
org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface),
contentDescription = "Run"
)
}
val showAppMenu = remember { mutableStateOf(false) }
Box {
IconButton(onClick = {
showAppMenu.value = true
}) {
Icon(
Icons.Filled.Menu,
contentDescription = "Menu"
)
}
DropdownMenu(
expanded = showAppMenu.value,
onDismissRequest = { showAppMenu.value = false }) {
DropdownMenuItem(text = {
Text(text = "Clear PPTC Cache")
}, onClick = {
showAppMenu.value = false
viewModel.mainViewModel?.clearPptcCache(viewModel.mainViewModel?.selected?.titleId ?: "")
})
DropdownMenuItem(text = {
Text(text = "Purge Shader Cache")
}, onClick = {
showAppMenu.value = false
viewModel.mainViewModel?.purgeShaderCache(viewModel.mainViewModel?.selected?.titleId ?: "")
})
DropdownMenuItem(text = {
Text(text = "Manage Updates")
}, onClick = {
showAppMenu.value = false
openTitleUpdateDialog.value = true
})
DropdownMenuItem(text = {
Text(text = "Manage DLC")
}, onClick = {
showAppMenu.value = false
openDlcDialog.value = true
})
}
}
}
},
shape = CircleShape) {
Icon(
Icons.Filled.Add,
contentDescription = "Options"
)
}
floatingActionButton = {
FloatingActionButton(
onClick = {
viewModel.openGameFolder()
},
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(
org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface),
contentDescription = "Open Folder"
)
}
}
)
}
) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) {
val list = remember {
mutableStateListOf<GameModel>()
}
if(refresh.value) {
if (refresh.value) {
viewModel.setViewList(list)
refresh.value = false
showAppActions.value = false
}
val selectedModel = remember {
mutableStateOf(viewModel.mainViewModel?.selected)
}
LazyColumn(Modifier.fillMaxSize()) {
items(list) {
it.titleName?.apply {
if (this.isNotEmpty() && (query.value.trim().isEmpty() || this.lowercase(
if (this.isNotEmpty() && (query.value.trim()
.isEmpty() || this.lowercase(
Locale.getDefault()
)
.contains(query.value)))
GameItem(it, viewModel, showBottomSheet, showLoading)
.contains(query.value))
)
GameItem(
it,
viewModel,
showAppActions,
showLoading,
selectedModel
)
}
}
}
}
if(showLoading.value){
AlertDialog(onDismissRequest = { }) {
Card(modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier
if (showLoading.value) {
AlertDialog(onDismissRequest = { }) {
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()) {
Text(text = "Loading")
LinearProgressIndicator(modifier = Modifier
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.padding(top = 16.dp))
) {
Text(text = "Loading")
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
}
}
}
}
if(showBottomSheet.value) {
ModalBottomSheet(onDismissRequest = {
showBottomSheet.value = false
},
sheetState = sheetState) {
val openTitleUpdateDialog = remember { mutableStateOf(false) }
val openDlcDialog = remember { mutableStateOf(false) }
if(openTitleUpdateDialog.value) {
AlertDialog(onDismissRequest = {
openTitleUpdateDialog.value = false
}) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog)
}
}
if (openTitleUpdateDialog.value) {
AlertDialog(onDismissRequest = {
openTitleUpdateDialog.value = false
}) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
}
if(openDlcDialog.value) {
AlertDialog(onDismissRequest = {
openDlcDialog.value = false
}) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
DlcViews.Main(titleId, name, openDlcDialog)
}
}
}
Surface(color = MaterialTheme.colorScheme.surface,
modifier = Modifier.padding(16.dp)) {
Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
Card(
modifier = Modifier.padding(8.dp),
onClick = {
openTitleUpdateDialog.value = true
}
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
painter = painterResource(R.drawable.app_update),
contentDescription = "Game Updates",
tint = Color.Green,
modifier = Modifier
.width(48.dp)
.height(48.dp)
.align(Alignment.CenterHorizontally)
)
Text(text = "Game Updates",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface)
}
}
Card(
modifier = Modifier.padding(8.dp),
onClick = {
openDlcDialog.value = true
}
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
imageVector = org.ryujinx.android.Icons.download(),
contentDescription = "Game Dlc",
tint = Color.Green,
modifier = Modifier
.width(48.dp)
.height(48.dp)
.align(Alignment.CenterHorizontally)
)
Text(text = "Game DLC",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface)
}
}
}
}
}
if (openDlcDialog.value) {
AlertDialog(onDismissRequest = {
openDlcDialog.value = false
}) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
DlcViews.Main(titleId, name, openDlcDialog)
}
}
}
}
@ -369,16 +355,31 @@ class HomeViews {
fun GameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showSheet: MutableState<Boolean>,
showLoading: MutableState<Boolean>
showAppActions: 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
.fillMaxWidth()
.padding(8.dp)
.combinedClickable(
onClick = {
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
if (viewModel.mainViewModel?.selected != null) {
showAppActions.value = false
viewModel.mainViewModel?.apply {
selected = null
}
selectedModel.value = null
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
thread {
showLoading.value = true
val success =
@ -396,35 +397,40 @@ class HomeViews {
},
onLongClick = {
viewModel.mainViewModel?.selected = gameModel
showSheet.value = true
})) {
Row(modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween) {
showAppActions.value = true
selectedModel.value = gameModel
})
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Row {
if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000")
{
val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
val iconSource =
MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
val imageFile = File(iconSource)
if(imageFile.exists()) {
if (imageFile.exists()) {
val size = ImageSize / Resources.getSystem().displayMetrics.density
AsyncImage(model = imageFile,
AsyncImage(
model = imageFile,
contentDescription = gameModel.titleName + " icon",
modifier = Modifier
.padding(end = 8.dp)
.width(size.roundToInt().dp)
.height(size.roundToInt().dp))
}
else NotAvailableIcon()
modifier = Modifier
.padding(end = 8.dp)
.width(size.roundToInt().dp)
.height(size.roundToInt().dp)
)
} else NotAvailableIcon()
} else NotAvailableIcon()
Column{
Column {
Text(text = gameModel.titleName ?: "")
Text(text = gameModel.developer ?: "")
Text(text = gameModel.titleId ?: "")
}
}
Column{
Column {
Text(text = gameModel.version ?: "")
Text(text = String.format("%.3f", gameModel.fileSize))
}

View File

@ -16,12 +16,13 @@ class MainView {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
composable("user") { UserViews.Main(mainViewModel, navController) }
composable("settings") {
SettingViews.Main(
SettingsViewModel(
navController,
mainViewModel.activity
)
), mainViewModel
)
}
}

View File

@ -3,9 +3,7 @@ package org.ryujinx.android.views
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
@ -23,6 +21,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.KeyboardArrowUp
@ -33,6 +33,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
@ -51,16 +52,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity
import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
import kotlin.concurrent.thread
class SettingViews {
companion object {
const val EXPANSTION_TRANSITION_DURATION = 450
const val IMPORT_CODE = 12341
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Main(settingsViewModel: SettingsViewModel) {
fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) {
val loaded = remember {
mutableStateOf(false)
}
@ -134,7 +142,9 @@ class SettingViews {
}
})
}) { contentPadding ->
Column(modifier = Modifier.padding(contentPadding)) {
Column(modifier = Modifier
.padding(contentPadding)
.verticalScroll(rememberScrollState())) {
ExpandableView(onCardArrowClick = { }, title = "System") {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
@ -227,6 +237,121 @@ class SettingViews {
ignoreMissingServices.value = !ignoreMissingServices.value
})
}
val isImporting = remember {
mutableStateOf(false)
}
val showImportWarning = remember {
mutableStateOf(false)
}
val showImportCompletion = remember {
mutableStateOf(false)
}
var importFile = remember {
mutableStateOf<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") {
@ -257,14 +382,14 @@ class SettingViews {
text = "Resolution Scale",
modifier = Modifier.align(Alignment.CenterVertically)
)
Text(text = resScale.value.toString() +"x")
Text(text = resScale.value.toString() + "x")
}
Slider(value = resScale.value,
valueRange = 0.5f..4f,
steps = 6,
onValueChange = { it ->
resScale.value = it
} )
})
Row(
modifier = Modifier
.fillMaxWidth()
@ -276,9 +401,12 @@ class SettingViews {
text = "Enable Texture Recompression",
modifier = Modifier.align(Alignment.CenterVertically)
)
Switch(checked = enableTextureRecompression.value, onCheckedChange = {
enableTextureRecompression.value = !enableTextureRecompression.value
})
Switch(
checked = enableTextureRecompression.value,
onCheckedChange = {
enableTextureRecompression.value =
!enableTextureRecompression.value
})
}
Row(
modifier = Modifier
@ -290,7 +418,8 @@ class SettingViews {
var isDriverSelectorOpen = remember {
mutableStateOf(false)
}
var driverViewModel = VulkanDriverViewModel(settingsViewModel.activity)
var driverViewModel =
VulkanDriverViewModel(settingsViewModel.activity)
var isChanged = remember {
mutableStateOf(false)
}
@ -302,16 +431,16 @@ class SettingViews {
mutableStateOf(0)
}
if(refresh.value) {
if (refresh.value) {
isChanged.value = true
refresh.value = false
}
if(isDriverSelectorOpen.value){
if (isDriverSelectorOpen.value) {
AlertDialog(onDismissRequest = {
isDriverSelectorOpen.value = false
if(isChanged.value){
if (isChanged.value) {
driverViewModel.saveSelected()
}
}) {
@ -329,11 +458,15 @@ class SettingViews {
isChanged.value = true
}
Column {
Column (modifier = Modifier
.fillMaxWidth()
.height(300.dp)) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
@ -359,7 +492,9 @@ class SettingViews {
for (driver in drivers) {
var ind = driverIndex
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
@ -378,15 +513,21 @@ class SettingViews {
driverViewModel.selected =
driver.driverPath
}) {
Text(text = driver.libraryName,
Text(
text = driver.libraryName,
modifier = Modifier
.fillMaxWidth())
Text(text = driver.driverVersion,
.fillMaxWidth()
)
Text(
text = driver.driverVersion,
modifier = Modifier
.fillMaxWidth())
Text(text = driver.description,
.fillMaxWidth()
)
Text(
text = driver.description,
modifier = Modifier
.fillMaxWidth())
.fillMaxWidth()
)
}
}
@ -425,7 +566,7 @@ class SettingViews {
isDriverSelectorOpen.value = !isDriverSelectorOpen.value
},
modifier = Modifier.align(Alignment.CenterVertically)
){
) {
Text(text = "Drivers")
}
}
@ -485,24 +626,6 @@ class SettingViews {
}
}
val transition = updateTransition(transitionState, label = "transition")
val cardPaddingHorizontal by transition.animateDp({
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
}, label = "paddingTransition") {
if (mutableExpanded.value) 48.dp else 24.dp
}
val cardElevation by transition.animateDp({
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
}, label = "elevationTransition") {
if (mutableExpanded.value) 24.dp else 4.dp
}
val cardRoundedCorners by transition.animateDp({
tween(
durationMillis = EXPANSTION_TRANSITION_DURATION,
easing = FastOutSlowInEasing
)
}, label = "cornersTransition") {
if (mutableExpanded.value) 0.dp else 16.dp
}
val arrowRotationDegree by transition.animateFloat({
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
}, label = "rotationDegreeTransition") {
@ -514,7 +637,7 @@ class SettingViews {
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = cardPaddingHorizontal,
horizontal = 24.dp,
vertical = 8.dp
)
) {
@ -600,4 +723,4 @@ class SettingViews {
}
}
}
}
}

View File

@ -27,11 +27,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.ryujinx.android.viewmodels.TitleUpdateViewModel
import java.io.File
class TitleUpdateViews {
companion object {
@Composable
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>) {
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
val viewModel = TitleUpdateViewModel(titleId)
val selected = remember { mutableStateOf(0) }
@ -46,6 +47,9 @@ class TitleUpdateViews {
val copyProgress = remember {
mutableStateOf(0.0f)
}
var currentProgressName = remember {
mutableStateOf("Starting Copy")
}
Column {
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
Surface(
@ -77,7 +81,7 @@ class TitleUpdateViews {
mutableStateListOf<String>()
}
viewModel.setPaths(paths)
viewModel.setPaths(paths, canClose)
var index = 1
for (path in paths) {
val i = index
@ -86,7 +90,7 @@ class TitleUpdateViews {
selected = (selected.value == i),
onClick = { selected.value = i })
Text(
text = path,
text = File(path).name,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically)
@ -111,7 +115,7 @@ class TitleUpdateViews {
IconButton(
onClick = {
viewModel.Add()
viewModel.Add(isCopying, copyProgress, currentProgressName)
}
) {
Icon(
@ -122,22 +126,33 @@ class TitleUpdateViews {
}
}
var currentProgressName = remember {
mutableStateOf("Starting Copy")
}
if (isCopying.value) {
Text(text = "Copying updates to local storage")
Text(text = currentProgressName.value)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = copyProgress.value
)
Row {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = copyProgress.value
)
TextButton(
onClick = {
isCopying.value = false
canClose.value = true
viewModel.refreshPaths()
},
) {
Text("Cancel")
}
}
}
Spacer(modifier = Modifier.height(18.dp))
TextButton(
modifier = Modifier.align(Alignment.End),
onClick = {
viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName)
if (!isCopying.value) {
canClose.value = true
viewModel.save(selected.value, openDialog)
}
},
) {
Text("Save")

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>