1
0
forked from MeloNX/MeloNX

android - update version

android - stop loading game if update fails

android - add refresh list button

android - fix update error return code

android - fix compose error with game list

android - add progress indicator for game list

android - fix game list loading

android - adjust virtual controller button positions. added stick sensitivity option

android - bump version

android - fix vulkan driver install
This commit is contained in:
Emmanuel Hansen 2024-07-15 16:16:08 +00:00
parent d19582b055
commit 1a94e37816
12 changed files with 326 additions and 113 deletions

View File

@ -11,8 +11,8 @@ android {
applicationId "org.ryujinx.android" applicationId "org.ryujinx.android"
minSdk 30 minSdk 30
targetSdk 34 targetSdk 34
versionCode 10032 versionCode 10039
versionName '1.0.32' versionName '1.0.39'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad import com.swordfish.radialgamepad.library.RadialGamePad
@ -193,19 +194,25 @@ class GameController(var activity: Activity) {
} }
GamePadButtonInputId.LeftStick.ordinal -> { 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( RyujinxNative.jnaInstance.inputSetStickAxis(
1, 1,
ev.xAxis, x,
-ev.yAxis, -y,
this this
) )
} }
GamePadButtonInputId.RightStick.ordinal -> { 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( RyujinxNative.jnaInstance.inputSetStickAxis(
2, 2,
ev.xAxis, x,
-ev.yAxis, -y,
this this
) )
} }
@ -226,7 +233,8 @@ suspend fun <T> Flow<T>.safeCollect(
} }
private fun generateConfig(isLeft: Boolean): GamePadConfig { private fun generateConfig(isLeft: Boolean): GamePadConfig {
val distance = 0.05f val distance = 0.3f
val buttonScale = 1f
if (isLeft) { if (isLeft) {
return GamePadConfig( return GamePadConfig(
@ -240,9 +248,9 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
), ),
listOf( listOf(
SecondaryDialConfig.Cross( SecondaryDialConfig.Cross(
9, 10,
3, 3,
1.8f, 2.5f,
distance, distance,
CrossConfig( CrossConfig(
GamePadButtonInputId.DpadUp.ordinal, GamePadButtonInputId.DpadUp.ordinal,
@ -256,9 +264,9 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
SecondaryDialConfig.RotationProcessor() SecondaryDialConfig.RotationProcessor()
), ),
SecondaryDialConfig.SingleButton( SecondaryDialConfig.SingleButton(
0, 1,
1f, buttonScale,
0.05f, distance,
ButtonConfig( ButtonConfig(
GamePadButtonInputId.Minus.ordinal, GamePadButtonInputId.Minus.ordinal,
"-", "-",
@ -274,7 +282,7 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
), ),
SecondaryDialConfig.DoubleButton( SecondaryDialConfig.DoubleButton(
2, 2,
0.05f, distance,
ButtonConfig( ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal, GamePadButtonInputId.LeftShoulder.ordinal,
"L", "L",
@ -289,9 +297,9 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
SecondaryDialConfig.RotationProcessor() SecondaryDialConfig.RotationProcessor()
), ),
SecondaryDialConfig.SingleButton( SecondaryDialConfig.SingleButton(
8, 9,
1f, buttonScale,
0.05f, distance,
ButtonConfig( ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal, GamePadButtonInputId.LeftTrigger.ordinal,
"ZL", "ZL",
@ -362,8 +370,8 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
SecondaryDialConfig.Stick( SecondaryDialConfig.Stick(
7, 7,
2, 2,
3f, 2f,
0.05f, distance,
GamePadButtonInputId.RightStick.ordinal, GamePadButtonInputId.RightStick.ordinal,
GamePadButtonInputId.RightStickButton.ordinal, GamePadButtonInputId.RightStickButton.ordinal,
null, null,
@ -373,8 +381,8 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
), ),
SecondaryDialConfig.SingleButton( SecondaryDialConfig.SingleButton(
6, 6,
1f, buttonScale,
0.05f, distance,
ButtonConfig( ButtonConfig(
GamePadButtonInputId.Plus.ordinal, GamePadButtonInputId.Plus.ordinal,
"+", "+",
@ -390,7 +398,7 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
), ),
SecondaryDialConfig.DoubleButton( SecondaryDialConfig.DoubleButton(
3, 3,
0.05f, distance,
ButtonConfig( ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal, GamePadButtonInputId.RightShoulder.ordinal,
"R", "R",
@ -406,8 +414,8 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
), ),
SecondaryDialConfig.SingleButton( SecondaryDialConfig.SingleButton(
9, 9,
1f, buttonScale,
0.05f, distance,
ButtonConfig( ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal, GamePadButtonInputId.RightTrigger.ordinal,
"ZR", "ZR",

View File

@ -23,6 +23,61 @@ 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 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 { fun listView(color: Color): ImageVector {
return remember { return remember {
ImageVector.Builder( ImageVector.Builder(

View File

@ -59,9 +59,14 @@ class GameModel(var file: DocumentFile, val context: Context) {
val uri = Uri.parse(vm.data?.selected) val uri = Uri.parse(vm.data?.selected)
val file = DocumentFile.fromSingleUri(context, uri) val file = DocumentFile.fromSingleUri(context, uri)
if (file?.exists() == true) { 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
}
} }
} }
} }

View File

@ -1,6 +1,8 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager 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.DocumentFileType
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.search 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 org.ryujinx.android.MainActivity
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -18,11 +24,11 @@ class HomeViewModel(
) { ) {
private var shouldReload: Boolean = false private var shouldReload: Boolean = false
private var savedFolder: String = "" private var savedFolder: String = ""
private var isLoading: Boolean = false
private var loadedCache: MutableList<GameModel> = mutableListOf() private var loadedCache: MutableList<GameModel> = mutableListOf()
private var gameFolderPath: DocumentFile? = null private var gameFolderPath: DocumentFile? = null
private var sharedPref: SharedPreferences? = null private var sharedPref: SharedPreferences? = null
val gameList: SnapshotStateList<GameModel> = SnapshotStateList() val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
val isLoading: MutableState<Boolean> = mutableStateOf(false)
init { init {
if (activity != null) { if (activity != null) {
@ -59,19 +65,21 @@ class HomeViewModel(
shouldReload = true shouldReload = true
} }
@OptIn(DelicateCoroutinesApi::class)
private fun reloadGameList() { private fun reloadGameList() {
activity?.storageHelper ?: return activity?.storageHelper ?: return
if (isLoading)
return
val folder = gameFolderPath ?: return val folder = gameFolderPath ?: return
gameList.clear() shouldReload = false
if (isLoading.value)
return
gameList.clear()
loadedCache.clear()
isLoading.value = true
isLoading = true
thread { thread {
try { try {
loadedCache.clear()
for (file in folder.search(false, DocumentFileType.FILE)) { for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro") if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro")
activity.let { activity.let {
@ -79,14 +87,14 @@ class HomeViewModel(
if (item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true && item.titleName != "Unknown") { if (item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true && item.titleName != "Unknown") {
loadedCache.add(item) loadedCache.add(item)
gameList.add(item)
} }
} }
} }
isLoading = false
} finally { } finally {
isLoading = false isLoading.value = false
GlobalScope.launch(Dispatchers.Main){
filter("")
}
} }
} }
} }

View File

@ -66,14 +66,19 @@ class MainViewModel(val activity: MainActivity) {
firmwareVersion = RyujinxNative.jnaInstance.deviceGetInstalledFirmwareVersion() firmwareVersion = RyujinxNative.jnaInstance.deviceGetInstalledFirmwareVersion()
} }
fun loadGame(game: GameModel): Boolean { fun loadGame(game: GameModel): Int {
val descriptor = game.open() val descriptor = game.open()
if (descriptor == 0) if (descriptor == 0)
return false return 0
val update = game.openUpdate() val update = game.openUpdate()
if(update == -2)
{
return -2
}
gameModel = game gameModel = game
isMiiEditorLaunched = false isMiiEditorLaunched = false
@ -87,7 +92,7 @@ class MainViewModel(val activity: MainActivity) {
) )
if (!success) if (!success)
return false return 0
val nativeHelpers = NativeHelpers.instance val nativeHelpers = NativeHelpers.instance
val nativeInterop = NativeGraphicsInterop() val nativeInterop = NativeGraphicsInterop()
@ -141,7 +146,7 @@ class MainViewModel(val activity: MainActivity) {
driverHandle driverHandle
) )
if (!success) if (!success)
return false return 0
val semaphore = Semaphore(1, 0) val semaphore = Semaphore(1, 0)
runBlocking { runBlocking {
@ -168,12 +173,12 @@ class MainViewModel(val activity: MainActivity) {
} }
if (!success) if (!success)
return false return 0
success = success =
RyujinxNative.jnaInstance.deviceLoadDescriptor(descriptor, game.type.ordinal, update) RyujinxNative.jnaInstance.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
return success return if (success) 1 else 0
} }
fun loadMiiEditor(): Boolean { fun loadMiiEditor(): Boolean {

View File

@ -19,6 +19,7 @@ class QuickSettings(val activity: Activity) {
var useSwitchLayout: Boolean var useSwitchLayout: Boolean
var enableMotion: Boolean var enableMotion: Boolean
var enablePerformanceMode: Boolean var enablePerformanceMode: Boolean
var controllerStickSensitivity: Float
// Logs // Logs
var enableDebugLogs: Boolean var enableDebugLogs: Boolean
@ -49,6 +50,7 @@ class QuickSettings(val activity: Activity) {
useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true) useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true)
enableMotion = sharedPref.getBoolean("enableMotion", true) enableMotion = sharedPref.getBoolean("enableMotion", true)
enablePerformanceMode = sharedPref.getBoolean("enablePerformanceMode", true) enablePerformanceMode = sharedPref.getBoolean("enablePerformanceMode", true)
controllerStickSensitivity = sharedPref.getFloat("controllerStickSensitivity", 1.0f)
enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false) enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false)
enableStubLogs = sharedPref.getBoolean("enableStubLogs", false) enableStubLogs = sharedPref.getBoolean("enableStubLogs", false)
@ -78,6 +80,7 @@ class QuickSettings(val activity: Activity) {
editor.putBoolean("useSwitchLayout", useSwitchLayout) editor.putBoolean("useSwitchLayout", useSwitchLayout)
editor.putBoolean("enableMotion", enableMotion) editor.putBoolean("enableMotion", enableMotion)
editor.putBoolean("enablePerformanceMode", enablePerformanceMode) editor.putBoolean("enablePerformanceMode", enablePerformanceMode)
editor.putFloat("enablePerformanceMode", controllerStickSensitivity)
editor.putBoolean("enableDebugLogs", enableDebugLogs) editor.putBoolean("enableDebugLogs", enableDebugLogs)
editor.putBoolean("enableStubLogs", enableStubLogs) editor.putBoolean("enableStubLogs", enableStubLogs)

View File

@ -56,6 +56,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
useSwitchLayout: MutableState<Boolean>, useSwitchLayout: MutableState<Boolean>,
enableMotion: MutableState<Boolean>, enableMotion: MutableState<Boolean>,
enablePerformanceMode: MutableState<Boolean>, enablePerformanceMode: MutableState<Boolean>,
controllerStickSensitivity: MutableState<Float>,
enableDebugLogs: MutableState<Boolean>, enableDebugLogs: MutableState<Boolean>,
enableStubLogs: MutableState<Boolean>, enableStubLogs: MutableState<Boolean>,
enableInfoLogs: MutableState<Boolean>, enableInfoLogs: MutableState<Boolean>,
@ -82,6 +83,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true) useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true)
enableMotion.value = sharedPref.getBoolean("enableMotion", true) enableMotion.value = sharedPref.getBoolean("enableMotion", true)
enablePerformanceMode.value = sharedPref.getBoolean("enablePerformanceMode", false) enablePerformanceMode.value = sharedPref.getBoolean("enablePerformanceMode", false)
controllerStickSensitivity.value = sharedPref.getFloat("controllerStickSensitivity", 1.0f)
enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false) enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false)
enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false) enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false)
@ -109,6 +111,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
useSwitchLayout: MutableState<Boolean>, useSwitchLayout: MutableState<Boolean>,
enableMotion: MutableState<Boolean>, enableMotion: MutableState<Boolean>,
enablePerformanceMode: MutableState<Boolean>, enablePerformanceMode: MutableState<Boolean>,
controllerStickSensitivity: MutableState<Float>,
enableDebugLogs: MutableState<Boolean>, enableDebugLogs: MutableState<Boolean>,
enableStubLogs: MutableState<Boolean>, enableStubLogs: MutableState<Boolean>,
enableInfoLogs: MutableState<Boolean>, enableInfoLogs: MutableState<Boolean>,
@ -135,6 +138,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("useSwitchLayout", useSwitchLayout.value) editor.putBoolean("useSwitchLayout", useSwitchLayout.value)
editor.putBoolean("enableMotion", enableMotion.value) editor.putBoolean("enableMotion", enableMotion.value)
editor.putBoolean("enablePerformanceMode", enablePerformanceMode.value) editor.putBoolean("enablePerformanceMode", enablePerformanceMode.value)
editor.putFloat("controllerStickSensitivity", controllerStickSensitivity.value)
editor.putBoolean("enableDebugLogs", enableDebugLogs.value) editor.putBoolean("enableDebugLogs", enableDebugLogs.value)
editor.putBoolean("enableStubLogs", enableStubLogs.value) editor.putBoolean("enableStubLogs", enableStubLogs.value)

View File

@ -19,8 +19,8 @@ class TitleUpdateViewModel(val titleId: String) {
private var basePath: String private var basePath: String
private var updateJsonName = "updates.json" private var updateJsonName = "updates.json"
private var storageHelper: SimpleStorageHelper private var storageHelper: SimpleStorageHelper
var currentPaths: MutableList<String> = mutableListOf() private var currentPaths: MutableList<String> = mutableListOf()
var pathsState: SnapshotStateList<String>? = null private var pathsState: SnapshotStateList<String>? = null
companion object { companion object {
const val UpdateRequestCode = 1002 const val UpdateRequestCode = 1002

View File

@ -2,13 +2,13 @@ package org.ryujinx.android.viewmodels
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.openInputStream
import com.google.gson.Gson import com.google.gson.Gson
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.zip.ZipFile import java.util.zip.ZipInputStream
class VulkanDriverViewModel(val activity: MainActivity) { class VulkanDriverViewModel(val activity: MainActivity) {
var selected: String = "" var selected: String = ""
@ -105,39 +105,37 @@ class VulkanDriverViewModel(val activity: MainActivity) {
if (requestCode == DriverRequestCode) { if (requestCode == DriverRequestCode) {
val file = files.firstOrNull() val file = files.firstOrNull()
file?.apply { file?.apply {
val path = file.getAbsolutePath(storage.context) val stream = file.openInputStream(storage.context)
if (path.isNotEmpty()) { stream?.apply {
val name = file.name?.removeSuffix("." + file.extension) ?: "" val name = file.name?.removeSuffix("." + file.extension) ?: ""
val driverFolder = ensureDriverPath() val driverFolder = ensureDriverPath()
val extractionFolder = File(driverFolder.absolutePath + "/${name}") val extractionFolder = File(driverFolder.absolutePath + "/${name}")
extractionFolder.deleteRecursively() extractionFolder.deleteRecursively()
extractionFolder.mkdirs() extractionFolder.mkdirs()
ZipFile(path).use { zip -> ZipInputStream(stream).use { zip ->
zip.entries().asSequence().forEach { entry -> var entry = zip.nextEntry
while (entry != null) {
val filePath =
extractionFolder.absolutePath + File.separator + entry.name
zip.getInputStream(entry).use { input -> if (!entry.isDirectory) {
val filePath = File(filePath).delete()
extractionFolder.absolutePath + File.separator + entry.name val bos =
BufferedOutputStream(FileOutputStream(filePath))
if (!entry.isDirectory) { val bytesIn = ByteArray(4096)
File(filePath).delete() var read: Int
val bos = while (zip.read(bytesIn)
BufferedOutputStream(FileOutputStream(filePath)) .also { read = it } != -1
val bytesIn = ByteArray(4096) ) {
var read: Int bos.write(bytesIn, 0, read)
while (input.read(bytesIn)
.also { read = it } != -1
) {
bos.write(bytesIn, 0, read)
}
bos.close()
} else {
val dir = File(filePath)
dir.mkdir()
} }
bos.close()
} else {
val dir = File(filePath)
dir.mkdir()
} }
entry = zip.nextEntry
} }
} }
} }

View File

@ -3,6 +3,8 @@ package org.ryujinx.android.views
import android.content.res.Resources import android.content.res.Resources
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
@ -32,15 +34,17 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person 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.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
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
@ -61,8 +65,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap 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.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -99,6 +107,9 @@ class HomeViews {
val canClose = remember { mutableStateOf(true) } val canClose = remember { mutableStateOf(true) }
val openDlcDialog = remember { mutableStateOf(false) } val openDlcDialog = remember { mutableStateOf(false) }
var openAppBarExtra by remember { mutableStateOf(false) } var openAppBarExtra by remember { mutableStateOf(false) }
val showError = remember {
mutableStateOf("")
}
val selectedModel = remember { val selectedModel = remember {
mutableStateOf(viewModel.mainViewModel?.selected) mutableStateOf(viewModel.mainViewModel?.selected)
@ -110,6 +121,24 @@ class HomeViews {
mutableStateOf(true) 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( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { 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 -> ) { contentPadding ->
@ -309,56 +353,75 @@ class HomeViews {
val list = remember { val list = remember {
viewModel.gameList viewModel.gameList
} }
val isLoading = remember {
viewModel.isLoading
}
viewModel.filter(query.value) viewModel.filter(query.value)
if (!isPreview) { if (!isPreview) {
var settings = QuickSettings(viewModel.activity!!) var settings = QuickSettings(viewModel.activity!!)
if (settings.isGrid) { if (isLoading.value) {
val size = Box(modifier = Modifier.fillMaxSize())
GridImageSize / Resources.getSystem().displayMetrics.density {
LazyVerticalGrid( CircularProgressIndicator(
columns = GridCells.Adaptive(minSize = (size + 4).dp), modifier = Modifier
modifier = Modifier .width(64.dp)
.fillMaxSize() .align(Alignment.Center),
.padding(4.dp), color = MaterialTheme.colorScheme.secondary,
horizontalArrangement = Arrangement.SpaceEvenly trackColor = MaterialTheme.colorScheme.surfaceVariant
) { )
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
)
}
}
} }
} else { } else {
LazyColumn(Modifier.fillMaxSize()) { if (settings.isGrid) {
items(list) { val size =
it.titleName?.apply { GridImageSize / Resources.getSystem().displayMetrics.density
if (this.isNotEmpty() && (query.value.trim() LazyVerticalGrid(
.isEmpty() || this.lowercase( columns = GridCells.Adaptive(minSize = (size + 4).dp),
Locale.getDefault() 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)) GridGameItem(
)
Box(modifier = Modifier.animateItemPlacement()) {
ListGameItem(
it, it,
viewModel, viewModel,
showAppActions, showAppActions,
showLoading, showLoading,
selectedModel, 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 showLoading.value = true
val success = val success =
viewModel.mainViewModel.loadGame(viewModel.mainViewModel.selected!!) viewModel.mainViewModel.loadGame(viewModel.mainViewModel.selected!!)
if (success) { if (success == 1) {
launchOnUiThread { launchOnUiThread {
viewModel.mainViewModel.navigateToGame() viewModel.mainViewModel.navigateToGame()
} }
} else { } else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
viewModel.mainViewModel.selected!!.close() viewModel.mainViewModel.selected!!.close()
} }
showLoading.value = false showLoading.value = false
@ -527,7 +593,8 @@ class HomeViews {
viewModel: HomeViewModel, viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>, showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>, showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?> selectedModel: MutableState<GameModel?>,
showError: MutableState<String>
) { ) {
remember { remember {
selectedModel selectedModel
@ -555,11 +622,14 @@ class HomeViews {
showLoading.value = true showLoading.value = true
val success = val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false viewModel.mainViewModel?.loadGame(gameModel) ?: false
if (success) { if (success == 1) {
launchOnUiThread { launchOnUiThread {
viewModel.mainViewModel?.navigateToGame() viewModel.mainViewModel?.navigateToGame()
} }
} else { } else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
gameModel.close() gameModel.close()
} }
showLoading.value = false showLoading.value = false
@ -618,7 +688,8 @@ class HomeViews {
viewModel: HomeViewModel, viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>, showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>, showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?> selectedModel: MutableState<GameModel?>,
showError: MutableState<String>
) { ) {
remember { remember {
selectedModel selectedModel
@ -646,11 +717,14 @@ class HomeViews {
showLoading.value = true showLoading.value = true
val success = val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false viewModel.mainViewModel?.loadGame(gameModel) ?: false
if (success) { if (success == 1) {
launchOnUiThread { launchOnUiThread {
viewModel.mainViewModel?.navigateToGame() viewModel.mainViewModel?.navigateToGame()
} }
} else { } else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
gameModel.close() gameModel.close()
} }
showLoading.value = false showLoading.value = false

View File

@ -15,6 +15,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card 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.Label
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
@ -125,6 +131,7 @@ class SettingViews {
val useSwitchLayout = remember { mutableStateOf(true) } val useSwitchLayout = remember { mutableStateOf(true) }
val enableMotion = remember { mutableStateOf(true) } val enableMotion = remember { mutableStateOf(true) }
val enablePerformanceMode = remember { mutableStateOf(true) } val enablePerformanceMode = remember { mutableStateOf(true) }
val controllerStickSensitivity = remember { mutableStateOf(1.0f) }
val enableDebugLogs = remember { mutableStateOf(true) } val enableDebugLogs = remember { mutableStateOf(true) }
val enableStubLogs = remember { mutableStateOf(true) } val enableStubLogs = remember { mutableStateOf(true) }
@ -149,6 +156,7 @@ class SettingViews {
useSwitchLayout, useSwitchLayout,
enableMotion, enableMotion,
enablePerformanceMode, enablePerformanceMode,
controllerStickSensitivity,
enableDebugLogs, enableDebugLogs,
enableStubLogs, enableStubLogs,
enableInfoLogs, enableInfoLogs,
@ -184,6 +192,7 @@ class SettingViews {
useSwitchLayout, useSwitchLayout,
enableMotion, enableMotion,
enablePerformanceMode, enablePerformanceMode,
controllerStickSensitivity,
enableDebugLogs, enableDebugLogs,
enableStubLogs, enableStubLogs,
enableInfoLogs, enableInfoLogs,
@ -926,6 +935,49 @@ class SettingViews {
useSwitchLayout.value = !useSwitchLayout.value 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") { ExpandableView(onCardArrowClick = { }, title = "Log") {
@ -1094,6 +1146,7 @@ class SettingViews {
useSwitchLayout, useSwitchLayout,
enableMotion, enableMotion,
enablePerformanceMode, enablePerformanceMode,
controllerStickSensitivity,
enableDebugLogs, enableDebugLogs,
enableStubLogs, enableStubLogs,
enableInfoLogs, enableInfoLogs,