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 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0")
implementation "io.coil-kt:coil-compose:2.4.0"
testImplementation 'junit:junit:4.13.2'

View File

@ -7,6 +7,13 @@ 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 {
@ -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>?): String? {
private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor = uri?.let { context.contentResolver.query(it, projection, selection, selectionArgs,null) }
cursor = uri?.let {
context.contentResolver.query(
it,
projection,
selection,
selectionArgs,
null
)
}
if (cursor != null && cursor.moveToFirst()) {
val column_index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index)
@ -82,5 +107,56 @@ class Helpers {
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
fun importAppData(
file: DocumentFile,
isImporting: MutableState<Boolean>
) {
isImporting.value = true
try {
MainActivity.StorageHelper?.apply {
val stream = file.openInputStream(storage.context)
stream?.apply {
val folders = listOf("bis", "games", "profiles", "system")
for (f in folders) {
val dir = File(MainActivity.AppPath + "${File.separator}${f}")
if (dir.exists()) {
dir.deleteRecursively()
}
dir.mkdirs()
}
ZipInputStream(stream).use { zip ->
var count = 0
while (true) {
val header = zip.nextEntry ?: break
if (!folders.any { header.fileName.startsWith(it) }) {
continue
}
val filePath =
MainActivity.AppPath + File.separator + header.fileName
if (!header.isDirectory) {
val bos = BufferedOutputStream(FileOutputStream(filePath))
val bytesIn = ByteArray(4096)
var read: Int = 0
while (zip.read(bytesIn).also { read = it } > 0) {
bos.write(bytesIn, 0, read)
}
bos.close()
} else {
val dir = File(filePath)
dir.mkdir()
}
}
}
stream.close()
}
}
} finally {
isImporting.value = false
RyujinxNative().deviceReloadFilesystem()
}
}
}
}

View File

@ -23,6 +23,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 {

View File

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

View File

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

View File

@ -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,27 +70,30 @@ class HomeViews {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTopBar(
navController: NavHostController,
query: MutableState<String>,
refresh: MutableState<Boolean>
fun Home(
viewModel: HomeViewModel = HomeViewModel(),
navController: NavHostController? = null
) {
val topBarSize = remember {
mutableStateOf(0)
val showAppActions = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) }
val openTitleUpdateDialog = remember { mutableStateOf(false) }
val openDlcDialog = remember { mutableStateOf(false) }
val query = remember {
mutableStateOf("")
}
Column {
val showOptionsPopup = remember {
mutableStateOf(false)
val refresh = remember {
mutableStateOf(true)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
modifier = Modifier
.zIndex(1f)
.padding(top = 8.dp)
.onSizeChanged {
topBarSize.value = it.height
},
.fillMaxWidth()
.padding(top = 8.dp),
title = {
DockedSearchBar(
SearchBar(
modifier = Modifier.fillMaxWidth(),
shape = SearchBarDefaults.inputFieldShape,
query = query.value,
onQueryChange = {
@ -112,114 +109,130 @@ class HomeViews {
)
},
placeholder = {
Text(text = "Search Games")
}
) {
Text(text = "Ryujinx")
}
) { }
},
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
IconButton(onClick = {
}) {
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(
Icon(
Icons.Filled.Person,
contentDescription = "Run"
)
}
IconButton(
onClick = {
navController.navigate("settings")
},
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Start),
navController?.navigate("settings")
}
) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings"
)
Text(
text = "Settings", modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterVertically)
}
}
)
},
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"
)
}
@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 query = remember {
mutableStateOf("")
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
)
}
}
}
}
}
val refresh = remember {
mutableStateOf(true)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
navController?.apply {
MainTopBar(navController, query, refresh)
}
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(onClick = {
FloatingActionButton(
onClick = {
viewModel.openGameFolder()
},
shape = CircleShape) {
containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(
Icons.Filled.Add,
contentDescription = "Options"
org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface),
contentDescription = "Open Folder"
)
}
}
)
}
) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) {
@ -232,14 +245,25 @@ class HomeViews {
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
)
}
}
}
@ -247,31 +271,29 @@ class HomeViews {
if (showLoading.value) {
AlertDialog(onDismissRequest = { }) {
Card(modifier = Modifier
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()) {
Text(text = "Loading")
LinearProgressIndicator(modifier = Modifier
.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
@ -308,59 +330,6 @@ class HomeViews {
}
}
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)
}
}
}
}
}
}
}
}
}
@ -369,16 +338,31 @@ class HomeViews {
fun GameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showSheet: MutableState<Boolean>,
showLoading: MutableState<Boolean>
showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?>
) {
Surface(shape = MaterialTheme.shapes.medium,
remember {
selectedModel
}
val color =
if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
Surface(
shape = MaterialTheme.shapes.medium,
color = color,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.combinedClickable(
onClick = {
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
if (viewModel.mainViewModel?.selected != null) {
showAppActions.value = false
viewModel.mainViewModel?.apply {
selected = null
}
selectedModel.value = null
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
thread {
showLoading.value = true
val success =
@ -396,27 +380,32 @@ class HomeViews {
},
onLongClick = {
viewModel.mainViewModel?.selected = gameModel
showSheet.value = true
})) {
Row(modifier = Modifier
showAppActions.value = true
selectedModel.value = gameModel
})
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween) {
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()) {
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()
.height(size.roundToInt().dp)
)
} else NotAvailableIcon()
} else NotAvailableIcon()
Column {
Text(text = gameModel.titleName ?: "")

View File

@ -3,9 +3,7 @@ package org.ryujinx.android.views
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
@ -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<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") {
@ -276,8 +393,11 @@ 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(
@ -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)
}
@ -329,11 +450,15 @@ class SettingViews {
isChanged.value = true
}
Column {
Column (modifier = Modifier
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)) {
.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()
)
}
}
@ -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
)
) {