diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index bcab5a848..793c23dcd 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "org.ryujinx.android" minSdk 30 targetSdk 34 - versionCode 10032 - versionName '1.0.32' + versionCode 10039 + versionName '1.0.39' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt index 40460f0ef..a325ec8c4 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.swordfish.radialgamepad.library.RadialGamePad @@ -193,19 +194,25 @@ class GameController(var activity: Activity) { } GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) RyujinxNative.jnaInstance.inputSetStickAxis( 1, - ev.xAxis, - -ev.yAxis, + x, + -y, this ) } GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) RyujinxNative.jnaInstance.inputSetStickAxis( 2, - ev.xAxis, - -ev.yAxis, + x, + -y, this ) } @@ -226,7 +233,8 @@ suspend fun Flow.safeCollect( } private fun generateConfig(isLeft: Boolean): GamePadConfig { - val distance = 0.05f + val distance = 0.3f + val buttonScale = 1f if (isLeft) { return GamePadConfig( @@ -240,9 +248,9 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { ), listOf( SecondaryDialConfig.Cross( - 9, + 10, 3, - 1.8f, + 2.5f, distance, CrossConfig( GamePadButtonInputId.DpadUp.ordinal, @@ -256,9 +264,9 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { SecondaryDialConfig.RotationProcessor() ), SecondaryDialConfig.SingleButton( - 0, - 1f, - 0.05f, + 1, + buttonScale, + distance, ButtonConfig( GamePadButtonInputId.Minus.ordinal, "-", @@ -274,7 +282,7 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { ), SecondaryDialConfig.DoubleButton( 2, - 0.05f, + distance, ButtonConfig( GamePadButtonInputId.LeftShoulder.ordinal, "L", @@ -289,9 +297,9 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { SecondaryDialConfig.RotationProcessor() ), SecondaryDialConfig.SingleButton( - 8, - 1f, - 0.05f, + 9, + buttonScale, + distance, ButtonConfig( GamePadButtonInputId.LeftTrigger.ordinal, "ZL", @@ -362,8 +370,8 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { SecondaryDialConfig.Stick( 7, 2, - 3f, - 0.05f, + 2f, + distance, GamePadButtonInputId.RightStick.ordinal, GamePadButtonInputId.RightStickButton.ordinal, null, @@ -373,8 +381,8 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { ), SecondaryDialConfig.SingleButton( 6, - 1f, - 0.05f, + buttonScale, + distance, ButtonConfig( GamePadButtonInputId.Plus.ordinal, "+", @@ -390,7 +398,7 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { ), SecondaryDialConfig.DoubleButton( 3, - 0.05f, + distance, ButtonConfig( GamePadButtonInputId.RightShoulder.ordinal, "R", @@ -406,8 +414,8 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { ), SecondaryDialConfig.SingleButton( 9, - 1f, - 0.05f, + buttonScale, + distance, ButtonConfig( GamePadButtonInputId.RightTrigger.ordinal, "ZR", diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt index 674a4ebb1..6b0e27a8f 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Icons.kt @@ -23,6 +23,61 @@ class Icons { companion object { /// Icons exported from https://www.composables.com/icons @Composable + fun circle(color: Color): ImageVector { + return remember { + ImageVector.Builder( + name = "circle", + 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(20f, 36.375f) + quadToRelative(-3.375f, 0f, -6.375f, -1.292f) + quadToRelative(-3f, -1.291f, -5.208f, -3.521f) + quadToRelative(-2.209f, -2.229f, -3.5f, -5.208f) + quadTo(3.625f, 23.375f, 3.625f, 20f) + quadToRelative(0f, -3.417f, 1.292f, -6.396f) + quadToRelative(1.291f, -2.979f, 3.521f, -5.208f) + quadToRelative(2.229f, -2.229f, 5.208f, -3.5f) + reflectiveQuadTo(20f, 3.625f) + quadToRelative(3.417f, 0f, 6.396f, 1.292f) + quadToRelative(2.979f, 1.291f, 5.208f, 3.5f) + quadToRelative(2.229f, 2.208f, 3.5f, 5.187f) + reflectiveQuadTo(36.375f, 20f) + quadToRelative(0f, 3.375f, -1.292f, 6.375f) + quadToRelative(-1.291f, 3f, -3.5f, 5.208f) + quadToRelative(-2.208f, 2.209f, -5.187f, 3.5f) + quadToRelative(-2.979f, 1.292f, -6.396f, 1.292f) + close() + moveToRelative(0f, -2.625f) + quadToRelative(5.75f, 0f, 9.75f, -4.021f) + reflectiveQuadToRelative(4f, -9.729f) + quadToRelative(0f, -5.75f, -4f, -9.75f) + reflectiveQuadToRelative(-9.75f, -4f) + quadToRelative(-5.708f, 0f, -9.729f, 4f) + quadToRelative(-4.021f, 4f, -4.021f, 9.75f) + quadToRelative(0f, 5.708f, 4.021f, 9.729f) + quadTo(14.292f, 33.75f, 20f, 33.75f) + close() + moveTo(20f, 20f) + close() + } + }.build() + } + } + @Composable fun listView(color: Color): ImageVector { return remember { ImageVector.Builder( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt index 16c695e5a..714b65879 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt @@ -59,9 +59,14 @@ class GameModel(var file: DocumentFile, val context: Context) { val uri = Uri.parse(vm.data?.selected) val file = DocumentFile.fromSingleUri(context, uri) if (file?.exists() == true) { - updateDescriptor = context.contentResolver.openFileDescriptor(file.uri, "rw") + try { + updateDescriptor = + context.contentResolver.openFileDescriptor(file.uri, "rw") - return updateDescriptor?.fd ?: -1 + return updateDescriptor?.fd ?: -1 + } catch (e: Exception) { + return -2 + } } } } 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 6ccd80d3b..1a644c890 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 @@ -1,6 +1,8 @@ package org.ryujinx.android.viewmodels import android.content.SharedPreferences +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager @@ -8,6 +10,10 @@ import com.anggrayudi.storage.file.DocumentFileCompat import com.anggrayudi.storage.file.DocumentFileType import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.search +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.ryujinx.android.MainActivity import java.util.Locale import kotlin.concurrent.thread @@ -18,11 +24,11 @@ class HomeViewModel( ) { private var shouldReload: Boolean = false private var savedFolder: String = "" - private var isLoading: Boolean = false private var loadedCache: MutableList = mutableListOf() private var gameFolderPath: DocumentFile? = null private var sharedPref: SharedPreferences? = null val gameList: SnapshotStateList = SnapshotStateList() + val isLoading: MutableState = mutableStateOf(false) init { if (activity != null) { @@ -59,19 +65,21 @@ class HomeViewModel( shouldReload = true } + @OptIn(DelicateCoroutinesApi::class) private fun reloadGameList() { activity?.storageHelper ?: return - - if (isLoading) - return val folder = gameFolderPath ?: return - gameList.clear() + shouldReload = false + if (isLoading.value) + return + + gameList.clear() + loadedCache.clear() + isLoading.value = true - isLoading = true thread { try { - loadedCache.clear() for (file in folder.search(false, DocumentFileType.FILE)) { if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro") activity.let { @@ -79,14 +87,14 @@ class HomeViewModel( if (item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true && item.titleName != "Unknown") { loadedCache.add(item) - gameList.add(item) } } } - - isLoading = false } finally { - isLoading = false + isLoading.value = false + GlobalScope.launch(Dispatchers.Main){ + filter("") + } } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index 9b4563242..8b41d33ef 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -66,14 +66,19 @@ class MainViewModel(val activity: MainActivity) { firmwareVersion = RyujinxNative.jnaInstance.deviceGetInstalledFirmwareVersion() } - fun loadGame(game: GameModel): Boolean { + fun loadGame(game: GameModel): Int { val descriptor = game.open() if (descriptor == 0) - return false + return 0 val update = game.openUpdate() + if(update == -2) + { + return -2 + } + gameModel = game isMiiEditorLaunched = false @@ -87,7 +92,7 @@ class MainViewModel(val activity: MainActivity) { ) if (!success) - return false + return 0 val nativeHelpers = NativeHelpers.instance val nativeInterop = NativeGraphicsInterop() @@ -141,7 +146,7 @@ class MainViewModel(val activity: MainActivity) { driverHandle ) if (!success) - return false + return 0 val semaphore = Semaphore(1, 0) runBlocking { @@ -168,12 +173,12 @@ class MainViewModel(val activity: MainActivity) { } if (!success) - return false + return 0 success = RyujinxNative.jnaInstance.deviceLoadDescriptor(descriptor, game.type.ordinal, update) - return success + return if (success) 1 else 0 } fun loadMiiEditor(): Boolean { 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 6af7052ae..3434c2483 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 @@ -19,6 +19,7 @@ class QuickSettings(val activity: Activity) { var useSwitchLayout: Boolean var enableMotion: Boolean var enablePerformanceMode: Boolean + var controllerStickSensitivity: Float // Logs var enableDebugLogs: Boolean @@ -49,6 +50,7 @@ class QuickSettings(val activity: Activity) { useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true) enableMotion = sharedPref.getBoolean("enableMotion", true) enablePerformanceMode = sharedPref.getBoolean("enablePerformanceMode", true) + controllerStickSensitivity = sharedPref.getFloat("controllerStickSensitivity", 1.0f) enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false) enableStubLogs = sharedPref.getBoolean("enableStubLogs", false) @@ -78,6 +80,7 @@ class QuickSettings(val activity: Activity) { editor.putBoolean("useSwitchLayout", useSwitchLayout) editor.putBoolean("enableMotion", enableMotion) editor.putBoolean("enablePerformanceMode", enablePerformanceMode) + editor.putFloat("enablePerformanceMode", controllerStickSensitivity) editor.putBoolean("enableDebugLogs", enableDebugLogs) editor.putBoolean("enableStubLogs", enableStubLogs) 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 58b73546b..fdf282780 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 @@ -56,6 +56,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main useSwitchLayout: MutableState, enableMotion: MutableState, enablePerformanceMode: MutableState, + controllerStickSensitivity: MutableState, enableDebugLogs: MutableState, enableStubLogs: MutableState, enableInfoLogs: MutableState, @@ -82,6 +83,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true) enableMotion.value = sharedPref.getBoolean("enableMotion", true) enablePerformanceMode.value = sharedPref.getBoolean("enablePerformanceMode", false) + controllerStickSensitivity.value = sharedPref.getFloat("controllerStickSensitivity", 1.0f) enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false) enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false) @@ -109,6 +111,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main useSwitchLayout: MutableState, enableMotion: MutableState, enablePerformanceMode: MutableState, + controllerStickSensitivity: MutableState, enableDebugLogs: MutableState, enableStubLogs: MutableState, enableInfoLogs: MutableState, @@ -135,6 +138,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main editor.putBoolean("useSwitchLayout", useSwitchLayout.value) editor.putBoolean("enableMotion", enableMotion.value) editor.putBoolean("enablePerformanceMode", enablePerformanceMode.value) + editor.putFloat("controllerStickSensitivity", controllerStickSensitivity.value) editor.putBoolean("enableDebugLogs", enableDebugLogs.value) editor.putBoolean("enableStubLogs", enableStubLogs.value) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt index a5dc5b9be..1eea840a4 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt @@ -19,8 +19,8 @@ class TitleUpdateViewModel(val titleId: String) { private var basePath: String private var updateJsonName = "updates.json" private var storageHelper: SimpleStorageHelper - var currentPaths: MutableList = mutableListOf() - var pathsState: SnapshotStateList? = null + private var currentPaths: MutableList = mutableListOf() + private var pathsState: SnapshotStateList? = null companion object { const val UpdateRequestCode = 1002 diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt index 40eb56f59..a82dafe53 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/VulkanDriverViewModel.kt @@ -2,13 +2,13 @@ package org.ryujinx.android.viewmodels import androidx.compose.runtime.MutableState import com.anggrayudi.storage.file.extension -import com.anggrayudi.storage.file.getAbsolutePath +import com.anggrayudi.storage.file.openInputStream import com.google.gson.Gson import org.ryujinx.android.MainActivity import java.io.BufferedOutputStream import java.io.File import java.io.FileOutputStream -import java.util.zip.ZipFile +import java.util.zip.ZipInputStream class VulkanDriverViewModel(val activity: MainActivity) { var selected: String = "" @@ -105,39 +105,37 @@ class VulkanDriverViewModel(val activity: MainActivity) { if (requestCode == DriverRequestCode) { val file = files.firstOrNull() file?.apply { - val path = file.getAbsolutePath(storage.context) - if (path.isNotEmpty()) { + val stream = file.openInputStream(storage.context) + stream?.apply { val name = file.name?.removeSuffix("." + file.extension) ?: "" val driverFolder = ensureDriverPath() val extractionFolder = File(driverFolder.absolutePath + "/${name}") extractionFolder.deleteRecursively() extractionFolder.mkdirs() - ZipFile(path).use { zip -> - zip.entries().asSequence().forEach { entry -> + ZipInputStream(stream).use { zip -> + var entry = zip.nextEntry + while (entry != null) { + val filePath = + extractionFolder.absolutePath + File.separator + entry.name - zip.getInputStream(entry).use { input -> - val filePath = - extractionFolder.absolutePath + File.separator + entry.name - - if (!entry.isDirectory) { - File(filePath).delete() - val bos = - BufferedOutputStream(FileOutputStream(filePath)) - val bytesIn = ByteArray(4096) - var read: Int - while (input.read(bytesIn) - .also { read = it } != -1 - ) { - bos.write(bytesIn, 0, read) - } - bos.close() - } else { - val dir = File(filePath) - dir.mkdir() + if (!entry.isDirectory) { + File(filePath).delete() + val bos = + BufferedOutputStream(FileOutputStream(filePath)) + val bytesIn = ByteArray(4096) + var read: Int + while (zip.read(bytesIn) + .also { read = it } != -1 + ) { + bos.write(bytesIn, 0, read) } - + bos.close() + } else { + val dir = File(filePath) + dir.mkdir() } + entry = zip.nextEntry } } } 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 70d21ea5a..aab0d2c03 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 @@ -3,6 +3,8 @@ package org.ryujinx.android.views import android.content.res.Resources import android.graphics.BitmapFactory import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee @@ -32,15 +34,17 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Refresh 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.BasicAlertDialog import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -61,8 +65,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow @@ -99,6 +107,9 @@ class HomeViews { val canClose = remember { mutableStateOf(true) } val openDlcDialog = remember { mutableStateOf(false) } var openAppBarExtra by remember { mutableStateOf(false) } + val showError = remember { + mutableStateOf("") + } val selectedModel = remember { mutableStateOf(viewModel.mainViewModel?.selected) @@ -110,6 +121,24 @@ class HomeViews { mutableStateOf(true) } + var isFabVisible by remember { + mutableStateOf(true) + } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + if (available.y < -1) { + isFabVisible = false + } + if (available.y > 1) { + isFabVisible = true + } + return Offset.Zero + } + } + } + Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -170,6 +199,21 @@ class HomeViews { ) { } + }, + floatingActionButton = { + AnimatedVisibility(visible = isFabVisible, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 })) { + FloatingActionButton( + onClick = { + viewModel.requestReload() + viewModel.ensureReloadIfNecessary() + }, + shape = MaterialTheme.shapes.small + ) { + Icon(Icons.Default.Refresh, contentDescription = "refresh") + } + } } ) { contentPadding -> @@ -309,56 +353,75 @@ class HomeViews { val list = remember { viewModel.gameList } + val isLoading = remember { + viewModel.isLoading + } viewModel.filter(query.value) if (!isPreview) { 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() - .padding(4.dp), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - items(list) { - it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim() - .isEmpty() || this.lowercase(Locale.getDefault()) - .contains(query.value)) - ) - GridGameItem( - it, - viewModel, - showAppActions, - showLoading, - selectedModel - ) - } - } + if (isLoading.value) { + Box(modifier = Modifier.fillMaxSize()) + { + CircularProgressIndicator( + modifier = Modifier + .width(64.dp) + .align(Alignment.Center), + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant + ) } } else { - LazyColumn(Modifier.fillMaxSize()) { - items(list) { - it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim() - .isEmpty() || this.lowercase( - Locale.getDefault() + if (settings.isGrid) { + val size = + GridImageSize / Resources.getSystem().displayMetrics.density + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = (size + 4).dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .nestedScroll(nestedScrollConnection), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase(Locale.getDefault()) + .contains(query.value)) ) - .contains(query.value)) - ) - Box(modifier = Modifier.animateItemPlacement()) { - ListGameItem( + GridGameItem( it, viewModel, showAppActions, showLoading, selectedModel, + showError ) - } + } + } + } + } else { + LazyColumn(Modifier.fillMaxSize()) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase( + Locale.getDefault() + ) + .contains(query.value)) + ) + Box(modifier = Modifier.animateItemPlacement()) { + ListGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel, + showError + ) + } + } } } } @@ -443,11 +506,14 @@ class HomeViews { showLoading.value = true val success = viewModel.mainViewModel.loadGame(viewModel.mainViewModel.selected!!) - if (success) { + if (success == 1) { launchOnUiThread { viewModel.mainViewModel.navigateToGame() } } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" viewModel.mainViewModel.selected!!.close() } showLoading.value = false @@ -527,7 +593,8 @@ class HomeViews { viewModel: HomeViewModel, showAppActions: MutableState, showLoading: MutableState, - selectedModel: MutableState + selectedModel: MutableState, + showError: MutableState ) { remember { selectedModel @@ -555,11 +622,14 @@ class HomeViews { showLoading.value = true val success = viewModel.mainViewModel?.loadGame(gameModel) ?: false - if (success) { + if (success == 1) { launchOnUiThread { viewModel.mainViewModel?.navigateToGame() } } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" gameModel.close() } showLoading.value = false @@ -618,7 +688,8 @@ class HomeViews { viewModel: HomeViewModel, showAppActions: MutableState, showLoading: MutableState, - selectedModel: MutableState + selectedModel: MutableState, + showError: MutableState ) { remember { selectedModel @@ -646,11 +717,14 @@ class HomeViews { showLoading.value = true val success = viewModel.mainViewModel?.loadGame(gameModel) ?: false - if (success) { + if (success == 1) { launchOnUiThread { viewModel.mainViewModel?.navigateToGame() } } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" gameModel.close() } showLoading.value = false 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 b268e84e3..7b322df05 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 @@ -15,6 +15,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -24,6 +25,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState @@ -31,16 +35,18 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.Label import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltip import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider @@ -125,6 +131,7 @@ class SettingViews { val useSwitchLayout = remember { mutableStateOf(true) } val enableMotion = remember { mutableStateOf(true) } val enablePerformanceMode = remember { mutableStateOf(true) } + val controllerStickSensitivity = remember { mutableStateOf(1.0f) } val enableDebugLogs = remember { mutableStateOf(true) } val enableStubLogs = remember { mutableStateOf(true) } @@ -149,6 +156,7 @@ class SettingViews { useSwitchLayout, enableMotion, enablePerformanceMode, + controllerStickSensitivity, enableDebugLogs, enableStubLogs, enableInfoLogs, @@ -184,6 +192,7 @@ class SettingViews { useSwitchLayout, enableMotion, enablePerformanceMode, + controllerStickSensitivity, enableDebugLogs, enableStubLogs, enableInfoLogs, @@ -926,6 +935,49 @@ class SettingViews { useSwitchLayout.value = !useSwitchLayout.value }) } + + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Controller Stick Sensitivity", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Slider(modifier = Modifier.width(250.dp), value = controllerStickSensitivity.value, onValueChange = { + controllerStickSensitivity.value = it + }, valueRange = 0.1f..2f, + steps = 20, + interactionSource = interactionSource, + thumb = { + Label( + label = { + PlainTooltip(modifier = Modifier + .sizeIn(45.dp, 25.dp) + .wrapContentWidth()) { + Text("%.2f".format(controllerStickSensitivity.value)) + } + }, + interactionSource = interactionSource + ) { + Icon( + imageVector = org.ryujinx.android.Icons.circle( + color = MaterialTheme.colorScheme.primary + ), + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + tint = MaterialTheme.colorScheme.primary + ) + } + } + ) + } + } } ExpandableView(onCardArrowClick = { }, title = "Log") { @@ -1094,6 +1146,7 @@ class SettingViews { useSwitchLayout, enableMotion, enablePerformanceMode, + controllerStickSensitivity, enableDebugLogs, enableStubLogs, enableInfoLogs,