From 8e2eb5fd263dc2f50da705303bebd0ee81293d0e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 22 Oct 2023 17:06:38 +0000 Subject: [PATCH] clean main ui, add option to import app data --- src/RyujinxAndroid/app/build.gradle | 1 + .../main/java/org/ryujinx/android/Helpers.kt | 84 ++- .../main/java/org/ryujinx/android/Icons.kt | 106 ++++ .../java/org/ryujinx/android/RyujinxNative.kt | 1 + .../android/viewmodels/HomeViewModel.kt | 2 +- .../org/ryujinx/android/views/HomeViews.kt | 533 +++++++++--------- .../org/ryujinx/android/views/SettingViews.kt | 201 +++++-- 7 files changed, 608 insertions(+), 320 deletions(-) diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index c1e90b005..f23aca82c 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -99,6 +99,7 @@ dependencies { implementation "androidx.preference:preference-ktx:1.2.0" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2' implementation 'com.google.code.gson:gson:2.10.1' + implementation 'net.lingala.zip4j:zip4j:2.11.5' implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0") implementation "io.coil-kt:coil-compose:2.4.0" testImplementation 'junit:junit:4.13.2' diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt index 780a8fa6d..764d6edab 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -7,9 +7,16 @@ 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.file.openInputStream +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 +32,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 +46,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 } @@ -55,12 +67,25 @@ class Helpers { return null } - private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { + private fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array? + ): String? { var cursor: Cursor? = null val column = "_data" val projection = arrayOf(column) try { - cursor = uri?.let { context.contentResolver.query(it, projection, selection, selectionArgs,null) } + cursor = uri?.let { + context.contentResolver.query( + it, + projection, + selection, + selectionArgs, + null + ) + } if (cursor != null && cursor.moveToFirst()) { val column_index: Int = cursor.getColumnIndexOrThrow(column) return cursor.getString(column_index) @@ -82,5 +107,56 @@ class Helpers { private fun isMediaDocument(uri: Uri): Boolean { return "com.android.providers.media.documents" == uri.authority } + + fun importAppData( + file: DocumentFile, + isImporting: MutableState + ) { + isImporting.value = true + try { + MainActivity.StorageHelper?.apply { + val stream = file.openInputStream(storage.context) + stream?.apply { + val folders = listOf("bis", "games", "profiles", "system") + for (f in folders) { + val dir = File(MainActivity.AppPath + "${File.separator}${f}") + if (dir.exists()) { + dir.deleteRecursively() + } + + dir.mkdirs() + } + ZipInputStream(stream).use { zip -> + var count = 0 + while (true) { + val header = zip.nextEntry ?: break + if (!folders.any { header.fileName.startsWith(it) }) { + continue + } + val filePath = + MainActivity.AppPath + File.separator + header.fileName + + if (!header.isDirectory) { + val bos = BufferedOutputStream(FileOutputStream(filePath)) + val bytesIn = ByteArray(4096) + var read: Int = 0 + while (zip.read(bytesIn).also { read = it } > 0) { + bos.write(bytesIn, 0, read) + } + bos.close() + } else { + val dir = File(filePath) + dir.mkdir() + } + } + } + stream.close() + } + } + } finally { + isImporting.value = false + RyujinxNative().deviceReloadFilesystem() + } + } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt index 13820e150..9e666df73 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -23,6 +23,112 @@ 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 download(): ImageVector { val primaryColor = MaterialTheme.colorScheme.primary return remember { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index 98e54e1e4..b308785f7 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -38,6 +38,7 @@ class RyujinxNative { external fun graphicsRendererSetSize(width: Int, height: Int) external fun graphicsRendererSetVsync(enabled: Boolean) external fun graphicsRendererRunLoop() + external fun deviceReloadFilesystem() external fun inputInitialize(width: Int, height: Int) external fun inputSetClientSize(width: Int, height: Int) external fun inputSetTouchPoint(x: Int, y: Int) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt index e8666757f..dc34f6882 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt @@ -109,6 +109,6 @@ class HomeViewModel( fun setViewList(list: SnapshotStateList) { gameList = list - applyFilter() + reloadGameList() } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index 8f22d9e24..a4ca7d1b9 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -1,7 +1,6 @@ package org.ryujinx.android.views import android.content.res.Resources -import android.view.Gravity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -17,32 +16,30 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn 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.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 @@ -51,13 +48,10 @@ 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.text.font.FontWeight 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 @@ -76,125 +70,14 @@ class HomeViews { @OptIn(ExperimentalMaterial3Api::class) @Composable - fun MainTopBar( - navController: NavHostController, - query: MutableState, - refresh: MutableState + fun Home( + viewModel: HomeViewModel = HomeViewModel(), + navController: NavHostController? = null ) { - val topBarSize = remember { - mutableStateOf(0) - } - Column { - val showOptionsPopup = remember { - mutableStateOf(false) - } - TopAppBar( - modifier = Modifier - .zIndex(1f) - .padding(top = 8.dp) - .onSizeChanged { - topBarSize.value = it.height - }, - title = { - DockedSearchBar( - shape = SearchBarDefaults.inputFieldShape, - query = query.value, - onQueryChange = { - query.value = it - }, - onSearch = {}, - active = false, - onActiveChange = {}, - leadingIcon = { - Icon( - Icons.Filled.Search, - contentDescription = "Search Games" - ) - }, - placeholder = { - Text(text = "Search Games") - } - ) { - - } - }, - actions = { - IconButton( - onClick = { - refresh.value = true - } - ) { - Icon( - Icons.Filled.Refresh, - contentDescription = "Refresh" - ) - } - IconButton( - onClick = { - showOptionsPopup.value = true - } - ) { - Icon( - Icons.Filled.MoreVert, - contentDescription = "More" - ) - } - } - ) - Box { - if (showOptionsPopup.value) { - AlertDialog( - modifier = Modifier.padding( - top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp, - start = 16.dp, end = 16.dp - ), - onDismissRequest = { - showOptionsPopup.value = false - }) { - val dialogWindowProvider = - LocalView.current.parent as DialogWindowProvider - dialogWindowProvider.window.setGravity(Gravity.TOP) - Surface( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(16.dp), - shape = MaterialTheme.shapes.large, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column { - TextButton( - onClick = { - navController.navigate("settings") - }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Start), - ) { - Icon( - Icons.Filled.Settings, - contentDescription = "Settings" - ) - Text( - text = "Settings", modifier = Modifier - .padding(16.dp) - .align(Alignment.CenterVertically) - ) - } - } - } - } - } - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) { - val sheetState = rememberModalBottomSheetState() - val showBottomSheet = remember { mutableStateOf(false) } + val showAppActions = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) } + val openTitleUpdateDialog = remember { mutableStateOf(false) } + val openDlcDialog = remember { mutableStateOf(false) } val query = remember { mutableStateOf("") } @@ -204,23 +87,153 @@ class HomeViews { 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 = { + }) { + Icon( + Icons.Filled.Person, + contentDescription = "Run" + ) + } + 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) } + IconButton(onClick = { + showAppMenu.value = true + }) { + Icon( + Icons.Filled.Menu, + contentDescription = "Menu" + ) + } + + if (true) { + AlertDialog(onDismissRequest = { + showAppMenu.value = false + }) { + Surface(shape = MaterialTheme.shapes.medium, color = Color.Black) { + Row { + IconButton(onClick = { + openTitleUpdateDialog.value = true + }) { + Column( + modifier = Modifier + .fillMaxSize() + ) { + Icon( + painter = painterResource(R.drawable.app_update), + contentDescription = "Updates", + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .width(20.dp) + .height(20.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = "Updates", + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.onSurface + ) + + } + } + IconButton(onClick = { + openDlcDialog.value = true + }) { + Column( + modifier = Modifier + .fillMaxSize() + ) { + Icon( + imageVector = org.ryujinx.android.Icons.download(), + contentDescription = "Dlc", + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .width(20.dp) + .height(20.dp) + .align(Alignment.CenterHorizontally) + ) + Text( + text = "DLC", + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.onSurface + ) + + } + } + } + } + } + } + } }, - 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 { @@ -228,137 +241,93 @@ class HomeViews { } - if(refresh.value) { + if (refresh.value) { viewModel.setViewList(list) refresh.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) } - 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 +338,31 @@ class HomeViews { fun GameItem( gameModel: GameModel, viewModel: HomeViewModel, - showSheet: MutableState, - showLoading: MutableState + showAppActions: MutableState, + showLoading: MutableState, + selectedModel: MutableState ) { - Surface(shape = MaterialTheme.shapes.medium, + remember { + selectedModel + } + val color = + if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + + Surface( + shape = MaterialTheme.shapes.medium, + color = color, modifier = Modifier .fillMaxWidth() .padding(8.dp) .combinedClickable( onClick = { - if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { + if (viewModel.mainViewModel?.selected != null) { + showAppActions.value = false + viewModel.mainViewModel?.apply { + selected = null + } + selectedModel.value = null + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { thread { showLoading.value = true val success = @@ -396,35 +380,40 @@ class HomeViews { }, onLongClick = { viewModel.mainViewModel?.selected = gameModel - showSheet.value = true - })) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.SpaceBetween) { + showAppActions.value = true + selectedModel.value = gameModel + }) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { Row { - if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") - { - val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache + if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { + val iconSource = + MainActivity.AppPath + "/iconCache/" + gameModel.iconCache val imageFile = File(iconSource) - if(imageFile.exists()) { + if (imageFile.exists()) { val size = ImageSize / Resources.getSystem().displayMetrics.density - AsyncImage(model = imageFile, + AsyncImage( + model = imageFile, contentDescription = gameModel.titleName + " icon", - modifier = Modifier - .padding(end = 8.dp) - .width(size.roundToInt().dp) - .height(size.roundToInt().dp)) - } - else NotAvailableIcon() + modifier = Modifier + .padding(end = 8.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } else NotAvailableIcon() } else NotAvailableIcon() - Column{ + Column { Text(text = gameModel.titleName ?: "") Text(text = gameModel.developer ?: "") Text(text = gameModel.titleId ?: "") } } - Column{ + Column { Text(text = gameModel.version ?: "") Text(text = String.format("%.3f", gameModel.fileSize)) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index f7cc010c0..6cbf61680 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -3,9 +3,7 @@ package org.ryujinx.android.views import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition @@ -33,6 +31,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,12 +50,18 @@ 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.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 @@ -227,6 +232,118 @@ class SettingViews { ignoreMissingServices.value = !ignoreMissingServices.value }) } + val isImporting = remember { + mutableStateOf(false) + } + val showImportWarning = remember { + mutableStateOf(false) + } + val showImportCompletion = remember { + mutableStateOf(false) + } + var importFile = remember { + mutableStateOf(null) + } + Button(onClick = { + val storage = MainActivity.StorageHelper + storage?.apply { + val s = this.storage + val callBack = this.onFileSelected + onFileSelected = { requestCode, files -> + run { + onFileSelected = callBack + if (requestCode == IMPORT_CODE) { + val file = files.firstOrNull() + file?.apply { + if (this.extension == "zip") { + importFile.value = this + showImportWarning.value = true + } + } + } + } + } + openFilePicker( + IMPORT_CODE, + filterMimeTypes = arrayOf("application/zip") + ) + } + }) { + Text(text = "Import App Data") + } + + if (showImportWarning.value) { + AlertDialog(onDismissRequest = { + showImportWarning.value = false + importFile.value = null + }) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = "Importing app data will delete your current profile. Do you still want to continue?") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + val file = importFile.value + showImportWarning.value = false + importFile.value = null + file?.apply { + thread { + Helpers.importAppData(this, isImporting) + showImportCompletion.value = true + } + } + }, 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 + }) { + 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 +374,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 +393,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 +410,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 +423,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 +450,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 +484,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 +505,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 +558,7 @@ class SettingViews { isDriverSelectorOpen.value = !isDriverSelectorOpen.value }, modifier = Modifier.align(Alignment.CenterVertically) - ){ + ) { Text(text = "Drivers") } } @@ -485,24 +618,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 +629,7 @@ class SettingViews { modifier = Modifier .fillMaxWidth() .padding( - horizontal = cardPaddingHorizontal, + horizontal = 24.dp, vertical = 8.dp ) ) { @@ -600,4 +715,4 @@ class SettingViews { } } } -} \ No newline at end of file +}