clean main ui, add option to import app data

This commit is contained in:
Emmanuel Hansen 2023-10-22 17:06:38 +00:00
parent 34e52880da
commit 8e2eb5fd26
7 changed files with 608 additions and 320 deletions

View File

@ -99,6 +99,7 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0") implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0")
implementation "io.coil-kt:coil-compose:2.4.0" implementation "io.coil-kt:coil-compose:2.4.0"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'

View File

@ -7,9 +7,16 @@ import android.net.Uri
import android.os.Environment import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.openInputStream
import net.lingala.zip4j.io.inputstream.ZipInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
class Helpers { class Helpers {
companion object{ companion object {
fun getPath(context: Context, uri: Uri): String? { fun getPath(context: Context, uri: Uri): String? {
// DocumentProvider // DocumentProvider
@ -25,7 +32,10 @@ class Helpers {
} else if (isDownloadsDocument(uri)) { } else if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri) val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null) return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) { } else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri) val docId = DocumentsContract.getDocumentId(uri)
@ -36,9 +46,11 @@ class Helpers {
"image" -> { "image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} }
"video" -> { "video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
} }
"audio" -> { "audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
} }
@ -55,12 +67,25 @@ class Helpers {
return null return null
} }
private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? { private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null var cursor: Cursor? = null
val column = "_data" val column = "_data"
val projection = arrayOf(column) val projection = arrayOf(column)
try { try {
cursor = uri?.let { context.contentResolver.query(it, projection, selection, selectionArgs,null) } cursor = uri?.let {
context.contentResolver.query(
it,
projection,
selection,
selectionArgs,
null
)
}
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
val column_index: Int = cursor.getColumnIndexOrThrow(column) val column_index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index) return cursor.getString(column_index)
@ -82,5 +107,56 @@ class Helpers {
private fun isMediaDocument(uri: Uri): Boolean { private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority return "com.android.providers.media.documents" == uri.authority
} }
fun importAppData(
file: DocumentFile,
isImporting: MutableState<Boolean>
) {
isImporting.value = true
try {
MainActivity.StorageHelper?.apply {
val stream = file.openInputStream(storage.context)
stream?.apply {
val folders = listOf("bis", "games", "profiles", "system")
for (f in folders) {
val dir = File(MainActivity.AppPath + "${File.separator}${f}")
if (dir.exists()) {
dir.deleteRecursively()
}
dir.mkdirs()
}
ZipInputStream(stream).use { zip ->
var count = 0
while (true) {
val header = zip.nextEntry ?: break
if (!folders.any { header.fileName.startsWith(it) }) {
continue
}
val filePath =
MainActivity.AppPath + File.separator + header.fileName
if (!header.isDirectory) {
val bos = BufferedOutputStream(FileOutputStream(filePath))
val bytesIn = ByteArray(4096)
var read: Int = 0
while (zip.read(bytesIn).also { read = it } > 0) {
bos.write(bytesIn, 0, read)
}
bos.close()
} else {
val dir = File(filePath)
dir.mkdir()
}
}
}
stream.close()
}
}
} finally {
isImporting.value = false
RyujinxNative().deviceReloadFilesystem()
}
}
} }
} }

View File

@ -23,6 +23,112 @@ class Icons {
companion object{ companion object{
/// Icons exported from https://www.composables.com/icons /// Icons exported from https://www.composables.com/icons
@Composable @Composable
fun playArrow(color: Color): ImageVector {
return remember {
ImageVector.Builder(
name = "play_arrow",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(color),
fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(15.542f, 30f)
quadToRelative(-0.667f, 0.458f, -1.334f, 0.062f)
quadToRelative(-0.666f, -0.395f, -0.666f, -1.187f)
verticalLineTo(10.917f)
quadToRelative(0f, -0.75f, 0.666f, -1.146f)
quadToRelative(0.667f, -0.396f, 1.334f, 0.062f)
lineToRelative(14.083f, 9f)
quadToRelative(0.583f, 0.375f, 0.583f, 1.084f)
quadToRelative(0f, 0.708f, -0.583f, 1.083f)
close()
moveToRelative(0.625f, -10.083f)
close()
moveToRelative(0f, 6.541f)
lineToRelative(10.291f, -6.541f)
lineToRelative(-10.291f, -6.542f)
close()
}
}.build()
}
}
@Composable
fun folderOpen(color: Color): ImageVector {
return remember {
ImageVector.Builder(
name = "folder_open",
defaultWidth = 40.0.dp,
defaultHeight = 40.0.dp,
viewportWidth = 40.0f,
viewportHeight = 40.0f
).apply {
path(
fill = SolidColor(color),
fillAlpha = 1f,
stroke = null,
strokeAlpha = 1f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1f,
pathFillType = PathFillType.NonZero
) {
moveTo(6.25f, 33.125f)
quadToRelative(-1.083f, 0f, -1.854f, -0.792f)
quadToRelative(-0.771f, -0.791f, -0.771f, -1.875f)
verticalLineTo(9.667f)
quadToRelative(0f, -1.084f, 0.771f, -1.854f)
quadToRelative(0.771f, -0.771f, 1.854f, -0.771f)
horizontalLineToRelative(10.042f)
quadToRelative(0.541f, 0f, 1.041f, 0.208f)
quadToRelative(0.5f, 0.208f, 0.834f, 0.583f)
lineToRelative(1.875f, 1.834f)
horizontalLineTo(33.75f)
quadToRelative(1.083f, 0f, 1.854f, 0.791f)
quadToRelative(0.771f, 0.792f, 0.771f, 1.834f)
horizontalLineTo(18.917f)
lineTo(16.25f, 9.667f)
horizontalLineToRelative(-10f)
verticalLineTo(30.25f)
lineToRelative(3.542f, -13.375f)
quadToRelative(0.25f, -0.875f, 0.979f, -1.396f)
quadToRelative(0.729f, -0.521f, 1.604f, -0.521f)
horizontalLineToRelative(23.25f)
quadToRelative(1.292f, 0f, 2.104f, 1.021f)
quadToRelative(0.813f, 1.021f, 0.438f, 2.271f)
lineToRelative(-3.459f, 12.833f)
quadToRelative(-0.291f, 1f, -1f, 1.521f)
quadToRelative(-0.708f, 0.521f, -1.75f, 0.521f)
close()
moveToRelative(2.708f, -2.667f)
horizontalLineToRelative(23.167f)
lineToRelative(3.417f, -12.875f)
horizontalLineTo(12.333f)
close()
moveToRelative(0f, 0f)
lineToRelative(3.375f, -12.875f)
lineToRelative(-3.375f, 12.875f)
close()
moveToRelative(-2.708f, -15.5f)
verticalLineTo(9.667f)
verticalLineToRelative(5.291f)
close()
}
}.build()
}
}
@Composable
fun download(): ImageVector { fun download(): ImageVector {
val primaryColor = MaterialTheme.colorScheme.primary val primaryColor = MaterialTheme.colorScheme.primary
return remember { return remember {

View File

@ -38,6 +38,7 @@ class RyujinxNative {
external fun graphicsRendererSetSize(width: Int, height: Int) external fun graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean) external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop() external fun graphicsRendererRunLoop()
external fun deviceReloadFilesystem()
external fun inputInitialize(width: Int, height: Int) external fun inputInitialize(width: Int, height: Int)
external fun inputSetClientSize(width: Int, height: Int) external fun inputSetClientSize(width: Int, height: Int)
external fun inputSetTouchPoint(x: Int, y: Int) external fun inputSetTouchPoint(x: Int, y: Int)

View File

@ -109,6 +109,6 @@ class HomeViewModel(
fun setViewList(list: SnapshotStateList<GameModel>) { fun setViewList(list: SnapshotStateList<GameModel>) {
gameList = list gameList = list
applyFilter() reloadGameList()
} }
} }

View File

@ -1,7 +1,6 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import android.content.res.Resources import android.content.res.Resources
import android.view.Gravity
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement 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.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
@ -51,13 +48,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread import com.anggrayudi.storage.extension.launchOnUiThread
@ -76,125 +70,14 @@ class HomeViews {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainTopBar( fun Home(
navController: NavHostController, viewModel: HomeViewModel = HomeViewModel(),
query: MutableState<String>, navController: NavHostController? = null
refresh: MutableState<Boolean>
) { ) {
val topBarSize = remember { val showAppActions = remember { mutableStateOf(false) }
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 showLoading = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) }
val openTitleUpdateDialog = remember { mutableStateOf(false) }
val openDlcDialog = remember { mutableStateOf(false) }
val query = remember { val query = remember {
mutableStateOf("") mutableStateOf("")
} }
@ -204,23 +87,153 @@ class HomeViews {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
navController?.apply { TopAppBar(
MainTopBar(navController, query, refresh) 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, bottomBar = {
floatingActionButton = { BottomAppBar(actions = {
FloatingActionButton(onClick = { if (showAppActions.value) {
viewModel.openGameFolder() 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) { floatingActionButton = {
Icon( FloatingActionButton(
Icons.Filled.Add, onClick = {
contentDescription = "Options" viewModel.openGameFolder()
) },
} containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(
org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface),
contentDescription = "Open Folder"
)
}
}
)
} }
) { contentPadding -> ) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) { Box(modifier = Modifier.padding(contentPadding)) {
val list = remember { val list = remember {
@ -228,137 +241,93 @@ class HomeViews {
} }
if(refresh.value) { if (refresh.value) {
viewModel.setViewList(list) viewModel.setViewList(list)
refresh.value = false refresh.value = false
} }
val selectedModel = remember {
mutableStateOf(viewModel.mainViewModel?.selected)
}
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(list) { items(list) {
it.titleName?.apply { it.titleName?.apply {
if (this.isNotEmpty() && (query.value.trim().isEmpty() || this.lowercase( if (this.isNotEmpty() && (query.value.trim()
.isEmpty() || this.lowercase(
Locale.getDefault() Locale.getDefault()
) )
.contains(query.value))) .contains(query.value))
GameItem(it, viewModel, showBottomSheet, showLoading) )
GameItem(
it,
viewModel,
showAppActions,
showLoading,
selectedModel
)
} }
} }
} }
} }
if(showLoading.value){ if (showLoading.value) {
AlertDialog(onDismissRequest = { }) { AlertDialog(onDismissRequest = { }) {
Card(modifier = Modifier Card(
.padding(16.dp) modifier = Modifier
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier
.padding(16.dp) .padding(16.dp)
.fillMaxWidth()) { .fillMaxWidth(),
Text(text = "Loading") shape = MaterialTheme.shapes.medium
LinearProgressIndicator(modifier = Modifier ) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp)) ) {
Text(text = "Loading")
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
} }
} }
} }
} }
if(showBottomSheet.value) {
ModalBottomSheet(onDismissRequest = {
showBottomSheet.value = false
},
sheetState = sheetState) {
val openTitleUpdateDialog = remember { mutableStateOf(false) }
val openDlcDialog = remember { mutableStateOf(false) }
if(openTitleUpdateDialog.value) { if (openTitleUpdateDialog.value) {
AlertDialog(onDismissRequest = { AlertDialog(onDismissRequest = {
openTitleUpdateDialog.value = false openTitleUpdateDialog.value = false
}) { }) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.wrapContentWidth() .wrapContentWidth()
.wrapContentHeight(), .wrapContentHeight(),
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation tonalElevation = AlertDialogDefaults.TonalElevation
) { ) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: "" val name = viewModel.mainViewModel?.selected?.titleName ?: ""
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog) TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog)
}
}
} }
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)) { if (openDlcDialog.value) {
Column(modifier = Modifier.fillMaxSize()) { AlertDialog(onDismissRequest = {
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { openDlcDialog.value = false
Card( }) {
modifier = Modifier.padding(8.dp), Surface(
onClick = { modifier = Modifier
openTitleUpdateDialog.value = true .wrapContentWidth()
} .wrapContentHeight(),
) { shape = MaterialTheme.shapes.large,
Column(modifier = Modifier.padding(16.dp)) { tonalElevation = AlertDialogDefaults.TonalElevation
Icon( ) {
painter = painterResource(R.drawable.app_update), val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
contentDescription = "Game Updates", val name = viewModel.mainViewModel?.selected?.titleName ?: ""
tint = Color.Green, DlcViews.Main(titleId, name, openDlcDialog)
modifier = Modifier
.width(48.dp)
.height(48.dp)
.align(Alignment.CenterHorizontally)
)
Text(text = "Game Updates",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface)
}
}
Card(
modifier = Modifier.padding(8.dp),
onClick = {
openDlcDialog.value = true
}
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
imageVector = org.ryujinx.android.Icons.download(),
contentDescription = "Game Dlc",
tint = Color.Green,
modifier = Modifier
.width(48.dp)
.height(48.dp)
.align(Alignment.CenterHorizontally)
)
Text(text = "Game DLC",
modifier = Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface)
}
}
}
}
} }
} }
} }
} }
@ -369,16 +338,31 @@ class HomeViews {
fun GameItem( fun GameItem(
gameModel: GameModel, gameModel: GameModel,
viewModel: HomeViewModel, viewModel: HomeViewModel,
showSheet: MutableState<Boolean>, showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean> showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?>
) { ) {
Surface(shape = MaterialTheme.shapes.medium, remember {
selectedModel
}
val color =
if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
Surface(
shape = MaterialTheme.shapes.medium,
color = color,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { if (viewModel.mainViewModel?.selected != null) {
showAppActions.value = false
viewModel.mainViewModel?.apply {
selected = null
}
selectedModel.value = null
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
thread { thread {
showLoading.value = true showLoading.value = true
val success = val success =
@ -396,35 +380,40 @@ class HomeViews {
}, },
onLongClick = { onLongClick = {
viewModel.mainViewModel?.selected = gameModel viewModel.mainViewModel?.selected = gameModel
showSheet.value = true showAppActions.value = true
})) { selectedModel.value = gameModel
Row(modifier = Modifier })
.fillMaxWidth() ) {
.padding(8.dp), Row(
horizontalArrangement = Arrangement.SpaceBetween) { modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Row { Row {
if(!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
{ val iconSource =
val iconSource = MainActivity.AppPath + "/iconCache/" + gameModel.iconCache MainActivity.AppPath + "/iconCache/" + gameModel.iconCache
val imageFile = File(iconSource) val imageFile = File(iconSource)
if(imageFile.exists()) { if (imageFile.exists()) {
val size = ImageSize / Resources.getSystem().displayMetrics.density val size = ImageSize / Resources.getSystem().displayMetrics.density
AsyncImage(model = imageFile, AsyncImage(
model = imageFile,
contentDescription = gameModel.titleName + " icon", contentDescription = gameModel.titleName + " icon",
modifier = Modifier modifier = Modifier
.padding(end = 8.dp) .padding(end = 8.dp)
.width(size.roundToInt().dp) .width(size.roundToInt().dp)
.height(size.roundToInt().dp)) .height(size.roundToInt().dp)
} )
else NotAvailableIcon() } else NotAvailableIcon()
} else NotAvailableIcon() } else NotAvailableIcon()
Column{ Column {
Text(text = gameModel.titleName ?: "") Text(text = gameModel.titleName ?: "")
Text(text = gameModel.developer ?: "") Text(text = gameModel.developer ?: "")
Text(text = gameModel.titleId ?: "") Text(text = gameModel.titleId ?: "")
} }
} }
Column{ Column {
Text(text = gameModel.version ?: "") Text(text = gameModel.version ?: "")
Text(text = String.format("%.3f", gameModel.fileSize)) Text(text = String.format("%.3f", gameModel.fileSize))
} }

View File

@ -3,9 +3,7 @@ package org.ryujinx.android.views
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
@ -33,6 +31,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -51,12 +50,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel import org.ryujinx.android.viewmodels.VulkanDriverViewModel
import kotlin.concurrent.thread
class SettingViews { class SettingViews {
companion object { companion object {
const val EXPANSTION_TRANSITION_DURATION = 450 const val EXPANSTION_TRANSITION_DURATION = 450
const val IMPORT_CODE = 12341
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -227,6 +232,118 @@ class SettingViews {
ignoreMissingServices.value = !ignoreMissingServices.value ignoreMissingServices.value = !ignoreMissingServices.value
}) })
} }
val isImporting = remember {
mutableStateOf(false)
}
val showImportWarning = remember {
mutableStateOf(false)
}
val showImportCompletion = remember {
mutableStateOf(false)
}
var importFile = remember {
mutableStateOf<DocumentFile?>(null)
}
Button(onClick = {
val storage = MainActivity.StorageHelper
storage?.apply {
val s = this.storage
val callBack = this.onFileSelected
onFileSelected = { requestCode, files ->
run {
onFileSelected = callBack
if (requestCode == IMPORT_CODE) {
val file = files.firstOrNull()
file?.apply {
if (this.extension == "zip") {
importFile.value = this
showImportWarning.value = true
}
}
}
}
}
openFilePicker(
IMPORT_CODE,
filterMimeTypes = arrayOf("application/zip")
)
}
}) {
Text(text = "Import App Data")
}
if (showImportWarning.value) {
AlertDialog(onDismissRequest = {
showImportWarning.value = false
importFile.value = null
}) {
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = "Importing app data will delete your current profile. Do you still want to continue?")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
val file = importFile.value
showImportWarning.value = false
importFile.value = null
file?.apply {
thread {
Helpers.importAppData(this, isImporting)
showImportCompletion.value = true
}
}
}, 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") { ExpandableView(onCardArrowClick = { }, title = "Graphics") {
@ -257,14 +374,14 @@ class SettingViews {
text = "Resolution Scale", text = "Resolution Scale",
modifier = Modifier.align(Alignment.CenterVertically) modifier = Modifier.align(Alignment.CenterVertically)
) )
Text(text = resScale.value.toString() +"x") Text(text = resScale.value.toString() + "x")
} }
Slider(value = resScale.value, Slider(value = resScale.value,
valueRange = 0.5f..4f, valueRange = 0.5f..4f,
steps = 6, steps = 6,
onValueChange = { it -> onValueChange = { it ->
resScale.value = it resScale.value = it
} ) })
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -276,9 +393,12 @@ class SettingViews {
text = "Enable Texture Recompression", text = "Enable Texture Recompression",
modifier = Modifier.align(Alignment.CenterVertically) modifier = Modifier.align(Alignment.CenterVertically)
) )
Switch(checked = enableTextureRecompression.value, onCheckedChange = { Switch(
enableTextureRecompression.value = !enableTextureRecompression.value checked = enableTextureRecompression.value,
}) onCheckedChange = {
enableTextureRecompression.value =
!enableTextureRecompression.value
})
} }
Row( Row(
modifier = Modifier modifier = Modifier
@ -290,7 +410,8 @@ class SettingViews {
var isDriverSelectorOpen = remember { var isDriverSelectorOpen = remember {
mutableStateOf(false) mutableStateOf(false)
} }
var driverViewModel = VulkanDriverViewModel(settingsViewModel.activity) var driverViewModel =
VulkanDriverViewModel(settingsViewModel.activity)
var isChanged = remember { var isChanged = remember {
mutableStateOf(false) mutableStateOf(false)
} }
@ -302,16 +423,16 @@ class SettingViews {
mutableStateOf(0) mutableStateOf(0)
} }
if(refresh.value) { if (refresh.value) {
isChanged.value = true isChanged.value = true
refresh.value = false refresh.value = false
} }
if(isDriverSelectorOpen.value){ if (isDriverSelectorOpen.value) {
AlertDialog(onDismissRequest = { AlertDialog(onDismissRequest = {
isDriverSelectorOpen.value = false isDriverSelectorOpen.value = false
if(isChanged.value){ if (isChanged.value) {
driverViewModel.saveSelected() driverViewModel.saveSelected()
} }
}) { }) {
@ -329,11 +450,15 @@ class SettingViews {
isChanged.value = true isChanged.value = true
} }
Column { Column {
Column (modifier = Modifier Column(
.fillMaxWidth() modifier = Modifier
.height(300.dp)) { .fillMaxWidth()
.height(300.dp)
) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(8.dp), modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(
@ -359,7 +484,9 @@ class SettingViews {
for (driver in drivers) { for (driver in drivers) {
var ind = driverIndex var ind = driverIndex
Row( Row(
modifier = Modifier.fillMaxWidth().padding(8.dp), modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( RadioButton(
@ -378,15 +505,21 @@ class SettingViews {
driverViewModel.selected = driverViewModel.selected =
driver.driverPath driver.driverPath
}) { }) {
Text(text = driver.libraryName, Text(
text = driver.libraryName,
modifier = Modifier modifier = Modifier
.fillMaxWidth()) .fillMaxWidth()
Text(text = driver.driverVersion, )
Text(
text = driver.driverVersion,
modifier = Modifier modifier = Modifier
.fillMaxWidth()) .fillMaxWidth()
Text(text = driver.description, )
Text(
text = driver.description,
modifier = Modifier modifier = Modifier
.fillMaxWidth()) .fillMaxWidth()
)
} }
} }
@ -425,7 +558,7 @@ class SettingViews {
isDriverSelectorOpen.value = !isDriverSelectorOpen.value isDriverSelectorOpen.value = !isDriverSelectorOpen.value
}, },
modifier = Modifier.align(Alignment.CenterVertically) modifier = Modifier.align(Alignment.CenterVertically)
){ ) {
Text(text = "Drivers") Text(text = "Drivers")
} }
} }
@ -485,24 +618,6 @@ class SettingViews {
} }
} }
val transition = updateTransition(transitionState, label = "transition") val transition = updateTransition(transitionState, label = "transition")
val cardPaddingHorizontal by transition.animateDp({
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
}, label = "paddingTransition") {
if (mutableExpanded.value) 48.dp else 24.dp
}
val cardElevation by transition.animateDp({
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
}, label = "elevationTransition") {
if (mutableExpanded.value) 24.dp else 4.dp
}
val cardRoundedCorners by transition.animateDp({
tween(
durationMillis = EXPANSTION_TRANSITION_DURATION,
easing = FastOutSlowInEasing
)
}, label = "cornersTransition") {
if (mutableExpanded.value) 0.dp else 16.dp
}
val arrowRotationDegree by transition.animateFloat({ val arrowRotationDegree by transition.animateFloat({
tween(durationMillis = EXPANSTION_TRANSITION_DURATION) tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
}, label = "rotationDegreeTransition") { }, label = "rotationDegreeTransition") {
@ -514,7 +629,7 @@ class SettingViews {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
horizontal = cardPaddingHorizontal, horizontal = 24.dp,
vertical = 8.dp vertical = 8.dp
) )
) { ) {
@ -600,4 +715,4 @@ class SettingViews {
} }
} }
} }
} }