From bc6e5de50705014b2391a56ebf13eece14b63659 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 11 Nov 2023 21:17:41 +0000 Subject: [PATCH] android - add grid list option --- .../android/viewmodels/HomeViewModel.kt | 7 +- .../android/viewmodels/QuickSettings.kt | 2 + .../android/viewmodels/SettingsViewModel.kt | 10 +- .../org/ryujinx/android/views/HomeViews.kt | 341 ++++++++++++------ .../org/ryujinx/android/views/SettingViews.kt | 31 +- 5 files changed, 273 insertions(+), 118 deletions(-) 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 2419dbb37..133a500cc 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 @@ -85,8 +85,11 @@ class HomeViewModel( if (file.extension == "xci" || file.extension == "nsp") activity.let { val item = GameModel(file, it) - files.add(item) - gameList.add(item) + + if(item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true) { + files.add(item) + gameList.add(item) + } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt index 18300f52b..e37377c09 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt @@ -15,6 +15,7 @@ class QuickSettings(val activity: Activity) { var enableShaderCache: Boolean var enableTextureRecompression: Boolean var resScale : Float + var isGrid : Boolean private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) @@ -29,5 +30,6 @@ class QuickSettings(val activity: Activity) { enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) resScale = sharedPref.getFloat("resScale", 1f) useVirtualController = sharedPref.getBoolean("useVirtualController", true) + isGrid = sharedPref.getBoolean("isGrid", true) } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt index 5db75c5f9..5634a8392 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt @@ -27,7 +27,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enableShaderCache: MutableState, enableTextureRecompression: MutableState, resScale: MutableState, - useVirtualController: MutableState + useVirtualController: MutableState, + isGrid: MutableState, ) { @@ -41,6 +42,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) resScale.value = sharedPref.getFloat("resScale", 1f) useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) + isGrid.value = sharedPref.getBoolean("isGrid", true) } fun save( @@ -53,7 +55,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enableShaderCache: MutableState, enableTextureRecompression: MutableState, resScale: MutableState, - useVirtualController: MutableState + useVirtualController: MutableState, + isGrid: MutableState ){ val editor = sharedPref.edit() @@ -67,7 +70,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value) editor.putFloat("resScale", resScale.value) editor.putBoolean("useVirtualController", useVirtualController.value) + editor.putBoolean("isGrid", isGrid.value) editor.apply() } -} \ No newline at end of file +} 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 877aa8cb7..27a701861 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 @@ -4,6 +4,7 @@ import android.content.res.Resources import android.graphics.BitmapFactory import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -18,8 +19,12 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Menu @@ -54,12 +59,14 @@ 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.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.anggrayudi.storage.extension.launchOnUiThread import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.HomeViewModel +import org.ryujinx.android.viewmodels.QuickSettings import java.util.Base64 import java.util.Locale import kotlin.concurrent.thread @@ -67,7 +74,8 @@ import kotlin.math.roundToInt class HomeViews { companion object { - const val ImageSize = 150 + const val ListImageSize = 150 + const val GridImageSize = 256 @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -157,108 +165,108 @@ class HomeViews { bottomBar = { BottomAppBar( actions = { - if (showAppActions.value) { - IconButton(onClick = { - if(viewModel.mainViewModel?.selected != null) { - thread { - showLoading.value = true - val success = - viewModel.mainViewModel?.loadGame(viewModel.mainViewModel.selected!!) - ?: false - if (success) { - launchOnUiThread { - viewModel.mainViewModel?.navigateToGame() - } - } else { - viewModel.mainViewModel?.selected!!.close() - } - showLoading.value = false - } - } - }) { - Icon( - org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), - contentDescription = "Run" - ) - } - val showAppMenu = remember { mutableStateOf(false) } - Box { + if (showAppActions.value) { IconButton(onClick = { - showAppMenu.value = true + if (viewModel.mainViewModel?.selected != null) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(viewModel.mainViewModel.selected!!) + ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + viewModel.mainViewModel?.selected!!.close() + } + showLoading.value = false + } + } }) { Icon( - Icons.Filled.Menu, - contentDescription = "Menu" + 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 + }) + } + } + } + + /*\val showAppletMenu = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { + showAppletMenu.value = true + }) { + Icon( + org.ryujinx.android.Icons.applets(MaterialTheme.colorScheme.onSurface), + contentDescription = "Applets" ) } DropdownMenu( - expanded = showAppMenu.value, - onDismissRequest = { showAppMenu.value = false }) { + expanded = showAppletMenu.value, + onDismissRequest = { showAppletMenu.value = false }) { DropdownMenuItem(text = { - Text(text = "Clear PPTC Cache") + Text(text = "Launch Mii Editor") }, 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 + showAppletMenu.value = false + showLoading.value = true + thread { + val success = + viewModel.mainViewModel?.loadMiiEditor() ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else + viewModel.mainViewModel!!.isMiiEditorLaunched = false + showLoading.value = false + } }) } - } - } - - /*\val showAppletMenu = remember { mutableStateOf(false) } - Box { - IconButton(onClick = { - showAppletMenu.value = true - }) { - Icon( - org.ryujinx.android.Icons.applets(MaterialTheme.colorScheme.onSurface), - contentDescription = "Applets" - ) - } - DropdownMenu( - expanded = showAppletMenu.value, - onDismissRequest = { showAppletMenu.value = false }) { - DropdownMenuItem(text = { - Text(text = "Launch Mii Editor") - }, onClick = { - showAppletMenu.value = false - showLoading.value = true - thread { - val success = - viewModel.mainViewModel?.loadMiiEditor() ?: false - if (success) { - launchOnUiThread { - viewModel.mainViewModel?.navigateToGame() - } - } else - viewModel.mainViewModel!!.isMiiEditorLaunched = false - showLoading.value = false - } - }) - } - }*/ - }, + }*/ + }, floatingActionButton = { FloatingActionButton( onClick = { @@ -285,22 +293,51 @@ class HomeViews { val selectedModel = remember { mutableStateOf(viewModel.mainViewModel?.selected) } - LazyColumn(Modifier.fillMaxSize()) { - items(list) { - it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim() - .isEmpty() || this.lowercase( - Locale.getDefault() + var settings = QuickSettings(viewModel.activity!!) + + if (settings.isGrid) { + val size = GridImageSize / Resources.getSystem().displayMetrics.density + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = (size + 4).dp), + modifier = Modifier + .fillMaxSize() + ) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase( + Locale.getDefault() + ) + .contains(query.value)) ) - .contains(query.value)) - ) - GameItem( - it, - viewModel, - showAppActions, - showLoading, - selectedModel + GridGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel + ) + } + } + } + } else { + LazyColumn(Modifier.fillMaxSize()) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase( + Locale.getDefault() + ) + .contains(query.value)) ) + ListGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel + ) + } } } } @@ -372,7 +409,7 @@ class HomeViews { @OptIn(ExperimentalFoundationApi::class) @Composable - fun GameItem( + fun ListGameItem( gameModel: GameModel, viewModel: HomeViewModel, showAppActions: MutableState, @@ -432,7 +469,8 @@ class HomeViews { if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { if (gameModel.icon?.isNotEmpty() == true) { val pic = decoder.decode(gameModel.icon) - val size = ImageSize / Resources.getSystem().displayMetrics.density + val size = + ListImageSize / Resources.getSystem().displayMetrics.density Image( bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) .asImageBitmap(), @@ -458,9 +496,92 @@ class HomeViews { } } + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun GridGameItem( + gameModel: GameModel, + viewModel: HomeViewModel, + showAppActions: MutableState, + showLoading: MutableState, + selectedModel: MutableState + ) { + remember { + selectedModel + } + val color = + if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + + val decoder = Base64.getDecoder() + Surface( + shape = MaterialTheme.shapes.medium, + color = color, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .combinedClickable( + onClick = { + if (viewModel.mainViewModel?.selected != null) { + showAppActions.value = false + viewModel.mainViewModel?.apply { + selected = null + } + selectedModel.value = null + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(gameModel) ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + gameModel.close() + } + showLoading.value = false + } + } + }, + onLongClick = { + viewModel.mainViewModel?.selected = gameModel + showAppActions.value = true + selectedModel.value = gameModel + }) + ) { + Column { + if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { + if (gameModel.icon?.isNotEmpty() == true) { + val pic = decoder.decode(gameModel.icon) + val size = GridImageSize / Resources.getSystem().displayMetrics.density + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), + contentDescription = gameModel.titleName + " icon", + modifier = Modifier + .padding(0.dp) + .clip(RoundedCornerShape(16.dp)) + ) + } else NotAvailableIcon() + } else NotAvailableIcon() + Text( + text = gameModel.titleName ?: "N/A", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.basicMarquee() + ) + Text( + text = gameModel.developer ?: "N/A", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.basicMarquee() + ) + } + } + } + @Composable fun NotAvailableIcon() { - val size = ImageSize / Resources.getSystem().displayMetrics.density + val size = ListImageSize / Resources.getSystem().displayMetrics.density Icon( Icons.Filled.Add, contentDescription = "Options", 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 d1c17d4b6..ead155a51 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 @@ -103,6 +103,9 @@ class SettingViews { val useVirtualController = remember { mutableStateOf(true) } + val isGrid = remember { + mutableStateOf(true) + } if (!loaded.value) { settingsViewModel.initializeState( @@ -112,7 +115,8 @@ class SettingViews { enableShaderCache, enableTextureRecompression, resScale, - useVirtualController + useVirtualController, + isGrid ) loaded.value = true } @@ -134,7 +138,8 @@ class SettingViews { enableShaderCache, enableTextureRecompression, resScale, - useVirtualController + useVirtualController, + isGrid ) settingsViewModel.navController.popBackStack() }) { @@ -145,6 +150,25 @@ class SettingViews { Column(modifier = Modifier .padding(contentPadding) .verticalScroll(rememberScrollState())) { + ExpandableView(onCardArrowClick = { }, title = "App") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Use Grid", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = isGrid.value, onCheckedChange = { + isGrid.value = !isGrid.value + }) + } + } + } ExpandableView(onCardArrowClick = { }, title = "System") { Column(modifier = Modifier.fillMaxWidth()) { Row( @@ -602,7 +626,8 @@ class SettingViews { enableShaderCache, enableTextureRecompression, resScale, - useVirtualController + useVirtualController, + isGrid ) settingsViewModel.navController.popBackStack() }