android - add grid list option

This commit is contained in:
Emmanuel Hansen 2023-11-11 21:17:41 +00:00
parent e5d54d5374
commit bc6e5de507
5 changed files with 273 additions and 118 deletions

View File

@ -85,8 +85,11 @@ class HomeViewModel(
if (file.extension == "xci" || file.extension == "nsp") if (file.extension == "xci" || file.extension == "nsp")
activity.let { activity.let {
val item = GameModel(file, it) 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)
}
} }
} }

View File

@ -15,6 +15,7 @@ class QuickSettings(val activity: Activity) {
var enableShaderCache: Boolean var enableShaderCache: Boolean
var enableTextureRecompression: Boolean var enableTextureRecompression: Boolean
var resScale : Float var resScale : Float
var isGrid : Boolean
private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
@ -29,5 +30,6 @@ class QuickSettings(val activity: Activity) {
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
resScale = sharedPref.getFloat("resScale", 1f) resScale = sharedPref.getFloat("resScale", 1f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true) useVirtualController = sharedPref.getBoolean("useVirtualController", true)
isGrid = sharedPref.getBoolean("isGrid", true)
} }
} }

View File

@ -27,7 +27,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableShaderCache: MutableState<Boolean>, enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>, enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float>, resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean> useVirtualController: MutableState<Boolean>,
isGrid: MutableState<Boolean>,
) )
{ {
@ -41,6 +42,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
resScale.value = sharedPref.getFloat("resScale", 1f) resScale.value = sharedPref.getFloat("resScale", 1f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
isGrid.value = sharedPref.getBoolean("isGrid", true)
} }
fun save( fun save(
@ -53,7 +55,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableShaderCache: MutableState<Boolean>, enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>, enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float>, resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean> useVirtualController: MutableState<Boolean>,
isGrid: MutableState<Boolean>
){ ){
val editor = sharedPref.edit() val editor = sharedPref.edit()
@ -67,7 +70,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value) editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
editor.putFloat("resScale", resScale.value) editor.putFloat("resScale", resScale.value)
editor.putBoolean("useVirtualController", useVirtualController.value) editor.putBoolean("useVirtualController", useVirtualController.value)
editor.putBoolean("isGrid", isGrid.value)
editor.apply() editor.apply()
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.res.Resources
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -18,8 +19,12 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn 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.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu 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.draw.clip
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.anggrayudi.storage.extension.launchOnUiThread import com.anggrayudi.storage.extension.launchOnUiThread
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel import org.ryujinx.android.viewmodels.HomeViewModel
import org.ryujinx.android.viewmodels.QuickSettings
import java.util.Base64 import java.util.Base64
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -67,7 +74,8 @@ import kotlin.math.roundToInt
class HomeViews { class HomeViews {
companion object { companion object {
const val ImageSize = 150 const val ListImageSize = 150
const val GridImageSize = 256
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -157,108 +165,108 @@ class HomeViews {
bottomBar = { bottomBar = {
BottomAppBar( BottomAppBar(
actions = { actions = {
if (showAppActions.value) { 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 {
IconButton(onClick = { 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( Icon(
Icons.Filled.Menu, org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface),
contentDescription = "Menu" 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( DropdownMenu(
expanded = showAppMenu.value, expanded = showAppletMenu.value,
onDismissRequest = { showAppMenu.value = false }) { onDismissRequest = { showAppletMenu.value = false }) {
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text(text = "Clear PPTC Cache") Text(text = "Launch Mii Editor")
}, onClick = { }, onClick = {
showAppMenu.value = false showAppletMenu.value = false
viewModel.mainViewModel?.clearPptcCache( showLoading.value = true
viewModel.mainViewModel?.selected?.titleId ?: "" thread {
) val success =
}) viewModel.mainViewModel?.loadMiiEditor() ?: false
DropdownMenuItem(text = { if (success) {
Text(text = "Purge Shader Cache") launchOnUiThread {
}, onClick = { viewModel.mainViewModel?.navigateToGame()
showAppMenu.value = false }
viewModel.mainViewModel?.purgeShaderCache( } else
viewModel.mainViewModel?.selected?.titleId ?: "" viewModel.mainViewModel!!.isMiiEditorLaunched = false
) showLoading.value = false
}) }
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 = 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 = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
@ -285,22 +293,51 @@ class HomeViews {
val selectedModel = remember { val selectedModel = remember {
mutableStateOf(viewModel.mainViewModel?.selected) mutableStateOf(viewModel.mainViewModel?.selected)
} }
LazyColumn(Modifier.fillMaxSize()) { var settings = QuickSettings(viewModel.activity!!)
items(list) {
it.titleName?.apply { if (settings.isGrid) {
if (this.isNotEmpty() && (query.value.trim() val size = GridImageSize / Resources.getSystem().displayMetrics.density
.isEmpty() || this.lowercase( LazyVerticalGrid(
Locale.getDefault() 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)) GridGameItem(
) it,
GameItem( viewModel,
it, showAppActions,
viewModel, showLoading,
showAppActions, selectedModel
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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun GameItem( fun ListGameItem(
gameModel: GameModel, gameModel: GameModel,
viewModel: HomeViewModel, viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>, showAppActions: MutableState<Boolean>,
@ -432,7 +469,8 @@ class HomeViews {
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
if (gameModel.icon?.isNotEmpty() == true) { if (gameModel.icon?.isNotEmpty() == true) {
val pic = decoder.decode(gameModel.icon) val pic = decoder.decode(gameModel.icon)
val size = ImageSize / Resources.getSystem().displayMetrics.density val size =
ListImageSize / Resources.getSystem().displayMetrics.density
Image( Image(
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
.asImageBitmap(), .asImageBitmap(),
@ -458,9 +496,92 @@ class HomeViews {
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GridGameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?>
) {
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 @Composable
fun NotAvailableIcon() { fun NotAvailableIcon() {
val size = ImageSize / Resources.getSystem().displayMetrics.density val size = ListImageSize / Resources.getSystem().displayMetrics.density
Icon( Icon(
Icons.Filled.Add, Icons.Filled.Add,
contentDescription = "Options", contentDescription = "Options",

View File

@ -103,6 +103,9 @@ class SettingViews {
val useVirtualController = remember { val useVirtualController = remember {
mutableStateOf(true) mutableStateOf(true)
} }
val isGrid = remember {
mutableStateOf(true)
}
if (!loaded.value) { if (!loaded.value) {
settingsViewModel.initializeState( settingsViewModel.initializeState(
@ -112,7 +115,8 @@ class SettingViews {
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale, resScale,
useVirtualController useVirtualController,
isGrid
) )
loaded.value = true loaded.value = true
} }
@ -134,7 +138,8 @@ class SettingViews {
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale, resScale,
useVirtualController useVirtualController,
isGrid
) )
settingsViewModel.navController.popBackStack() settingsViewModel.navController.popBackStack()
}) { }) {
@ -145,6 +150,25 @@ class SettingViews {
Column(modifier = Modifier Column(modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
.verticalScroll(rememberScrollState())) { .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") { ExpandableView(onCardArrowClick = { }, title = "System") {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row( Row(
@ -602,7 +626,8 @@ class SettingViews {
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale, resScale,
useVirtualController useVirtualController,
isGrid
) )
settingsViewModel.navController.popBackStack() settingsViewModel.navController.popBackStack()
} }