diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index 0702d83db..c1e90b005 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -36,6 +36,7 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug } } compileOptions { @@ -74,6 +75,12 @@ tasks.named("preBuild") { } dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation platform('androidx.compose:compose-bom:2023.03.00') + implementation platform('androidx.compose:compose-bom:2023.03.00') + androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00') + androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00') runtimeOnly project(":libryujinx") implementation 'androidx.core:core-ktx:1.10.1' implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') @@ -92,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("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' androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index ad43a05f7..4a05d62e4 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ - + + @@ -12,23 +15,28 @@ tools:ignore="ScopedStorage" /> + + diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index 18d9fb67a..12a331b9d 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -224,4 +224,10 @@ extern "C" void debug_break(int code){ if(code >= 3) int r = 0; -} \ No newline at end of file +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_setTurboMode(JNIEnv *env, jobject thiz, jboolean enable) { + adrenotools_set_turbo(enable); +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt new file mode 100644 index 000000000..1a1bb96b4 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameActivity.kt @@ -0,0 +1,346 @@ +package org.ryujinx.android + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.KeyEvent +import android.view.MotionEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +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.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import compose.icons.CssGgIcons +import compose.icons.cssggicons.ToolbarBottom +import org.ryujinx.android.ui.theme.RyujinxAndroidTheme +import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings +import kotlin.math.abs +import kotlin.math.roundToInt + +class GameActivity : ComponentActivity() { + private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + MainActivity.mainViewModel!!.physicalControllerManager = physicalControllerManager + setContent { + RyujinxAndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + GameView(mainViewModel = MainActivity.mainViewModel!!) + } + } + } + } + + @SuppressLint("RestrictedApi") + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + event?.apply { + if(physicalControllerManager.onKeyEvent(this)) + return true + } + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + ev?.apply { + physicalControllerManager.onMotionEvent(this) + } + return super.dispatchGenericMotionEvent(ev) + } + + override fun onStop() { + super.onStop() + + NativeHelpers().setTurboMode(false) + force60HzRefreshRate(false) + } + + override fun onResume() { + super.onResume() + + setFullScreen(true) + NativeHelpers().setTurboMode(true) + force60HzRefreshRate(true) + } + + override fun onPause() { + super.onPause() + + NativeHelpers().setTurboMode(false) + force60HzRefreshRate(false) + } + + private fun force60HzRefreshRate(enable : Boolean) { + // Hack for MIUI devices since they don't support the standard Android APIs + try { + val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS") + setFpsIntent.putExtra("package_name", "org.ryujinx.android") + setFpsIntent.putExtra("isEnter", enable) + sendBroadcast(setFpsIntent) + } catch (_ : Exception) { + } + + if (enable) + display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) }?.let { window.attributes.preferredDisplayModeId = it.modeId } + else + display?.supportedModes?.maxByOrNull { it.refreshRate }?.let { window.attributes.preferredDisplayModeId = it.modeId } + } + + private fun setFullScreen(fullscreen: Boolean) { + requestedOrientation = + if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER + + val insets = WindowCompat.getInsetsController(window, window.decorView) + + insets.apply { + if (fullscreen) { + insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) + insets.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } + } + } + @Composable + fun GameView(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + GameHost(context, mainViewModel) + } + ) + GameOverlay(mainViewModel) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun GameOverlay(mainViewModel: MainViewModel) { + Box(modifier = Modifier.fillMaxSize()) { + GameStats(mainViewModel) + + val ryujinxNative = RyujinxNative() + + val showController = remember { + mutableStateOf(QuickSettings(this@GameActivity).useVirtualController) + } + val enableVsync = remember { + mutableStateOf(QuickSettings(this@GameActivity).enableVsync) + } + val showMore = remember { + mutableStateOf(false) + } + + // touch surface + Surface(color = Color.Transparent, modifier = Modifier + .fillMaxSize() + .padding(0.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (showController.value) + continue + + val change = event + .component1() + .firstOrNull() + change?.apply { + val position = this.position + + when (event.type) { + PointerEventType.Press -> { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + } + + PointerEventType.Release -> { + ryujinxNative.inputReleaseTouchPoint() + + } + + PointerEventType.Move -> { + ryujinxNative.inputSetTouchPoint( + position.x.roundToInt(), + position.y.roundToInt() + ) + + } + } + } + } + } + }) { + } + GameController.Compose(mainViewModel) + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(8.dp) + ) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = true + }) { + Icon( + imageVector = CssGgIcons.ToolbarBottom, + contentDescription = "Open Panel" + ) + } + } + + if(showMore.value){ + Popup(alignment = Alignment.BottomCenter, onDismissRequest = {showMore.value = false}) { + Surface(modifier = Modifier.padding(16.dp), + shape = MaterialTheme.shapes.medium) { + Row(modifier = Modifier.padding(8.dp)) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + showController.value = !showController.value + mainViewModel.controller?.setVisible(showController.value) + }) { + Icon( + imageVector = Icons.videoGame(), + contentDescription = "Toggle Virtual Pad" + ) + } + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + enableVsync.value = !enableVsync.value + RyujinxNative().graphicsRendererSetVsync(enableVsync.value) + }) { + Icon( + imageVector = Icons.vSync(), + tint = if(enableVsync.value) Color.Green else Color.Red, + contentDescription = "Toggle VSync" + ) + } + } + } + } + } + + val showBackNotice = remember { + mutableStateOf(false) + } + + BackHandler { + showBackNotice.value = true + } + + if (showBackNotice.value) { + AlertDialog(onDismissRequest = { showBackNotice.value = false }) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Are you sure you want to exit the game?") + Text(text = "All unsaved data will be lost!") + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Button(onClick = { + mainViewModel.closeGame() + setFullScreen(false) + finishActivity(0) + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Exit Game") + } + + Button(onClick = { + showBackNotice.value = false + }, modifier = Modifier.padding(16.dp)) { + Text(text = "Dismiss") + } + } + } + } + } + } + } + } + } + + @Composable + fun GameStats(mainViewModel: MainViewModel) { + val fifo = remember { + mutableStateOf(0.0) + } + val gameFps = remember { + mutableStateOf(0.0) + } + val gameTime = remember { + mutableStateOf(0.0) + } + + Surface( + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.surface.copy(0.4f) + ) { + Column { + var gameTimeVal = 0.0 + if (!gameTime.value.isInfinite()) + gameTimeVal = gameTime.value + Text(text = "${String.format("%.3f", fifo.value)} %") + Text(text = "${String.format("%.3f", gameFps.value)} FPS") + Text(text = "${String.format("%.3f", gameTimeVal)} ms") + } + } + + mainViewModel.setStatStates(fifo, gameFps, gameTime) + } +} 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 aaf97cd3e..f8781021e 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 @@ -46,15 +46,17 @@ class GameController(var activity: Activity) { return view } @Composable - fun Compose(viewModel: MainViewModel) : Unit - { + fun Compose(viewModel: MainViewModel) : Unit { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> val controller = GameController(viewModel.activity) val c = Create(context, viewModel.activity, controller) - viewModel.activity.lifecycleScope.apply { - viewModel.activity.lifecycleScope.launch { - val events = merge(controller.leftGamePad.events(),controller.rightGamePad.events()) + viewModel.activity.lifecycleScope.apply { + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) .shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily) events.safeCollect { controller.handleEvent(it) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt index 38cd171b1..76bc2c9f2 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -1,5 +1,6 @@ package org.ryujinx.android +import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.view.SurfaceHolder @@ -7,21 +8,19 @@ import android.view.SurfaceView import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.QuickSettings -import org.ryujinx.android.viewmodels.VulkanDriverViewModel -import java.io.File import kotlin.concurrent.thread -class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { +@SuppressLint("ViewConstructor") +class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { + private var game: GameModel? = null private var _isClosed: Boolean = false private var _renderingThreadWatcher: Thread? = null private var _height: Int = 0 private var _width: Int = 0 private var _updateThread: Thread? = null - private var nativeInterop: NativeGraphicsInterop? = null private var _guestThread: Thread? = null private var _isInit: Boolean = false private var _isStarted: Boolean = false - private var _nativeWindow: Long = 0 private var _nativeRyujinx: RyujinxNative = RyujinxNative() @@ -47,6 +46,11 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie _width = width _height = height + _nativeRyujinx.graphicsRendererSetSize( + width, + height + ) + if(_isStarted) { _nativeRyujinx.inputSetClientSize(width, height) @@ -69,7 +73,9 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie private fun start(surfaceHolder: SurfaceHolder) { mainViewModel.gameHost = this if(_isStarted) - return; + return + + game = mainViewModel.gameModel _nativeRyujinx.inputInitialize(width, height) @@ -82,7 +88,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie mainViewModel.controller?.connect() } - mainViewModel.activity.physicalControllerManager.connect() + mainViewModel.physicalControllerManager?.connect() _nativeRyujinx.graphicsRendererSetSize( surfaceHolder.surfaceFrame.width(), @@ -130,5 +136,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie } } _nativeRyujinx.graphicsRendererRunLoop() + + game?.close() } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt index e833b82d4..780a8fa6d 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -4,7 +4,6 @@ import android.content.ContentUris import android.content.Context import android.database.Cursor import android.net.Uri -import android.os.Build import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore @@ -12,10 +11,9 @@ import android.provider.MediaStore class Helpers { companion object{ fun getPath(context: Context, uri: Uri): String? { - val isKitKatorAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT // DocumentProvider - if (isKitKatorAbove && DocumentsContract.isDocumentUri(context, uri)) { + if (DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { val docId = DocumentsContract.getDocumentId(uri) @@ -57,7 +55,7 @@ class Helpers { return null } - fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { + private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { var cursor: Cursor? = null val column = "_data" val projection = arrayOf(column) @@ -73,16 +71,16 @@ class Helpers { return null } - fun isExternalStorageDocument(uri: Uri): Boolean { + private fun isExternalStorageDocument(uri: Uri): Boolean { return "com.android.externalstorage.documents" == uri.authority } - fun isDownloadsDocument(uri: Uri): Boolean { + private fun isDownloadsDocument(uri: Uri): Boolean { return "com.android.providers.downloads.documents" == uri.authority } - fun isMediaDocument(uri: Uri): Boolean { + private fun isMediaDocument(uri: Uri): Boolean { return "com.android.providers.media.documents" == uri.authority } } -} \ No newline at end of file +} 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 96aa04bba..13820e150 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 @@ -1,8 +1,12 @@ package org.ryujinx.android +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.SolidColor @@ -10,13 +14,17 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import compose.icons.CssGgIcons +import compose.icons.cssggicons.Games class Icons { companion object{ /// Icons exported from https://www.composables.com/icons @Composable - fun Download(): ImageVector { + fun download(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary return remember { ImageVector.Builder( name = "download", @@ -26,9 +34,9 @@ class Icons { viewportHeight = 40.0f ).apply { path( - fill = SolidColor(Color.Black), + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), fillAlpha = 1f, - stroke = null, strokeAlpha = 1f, strokeLineWidth = 1.0f, strokeLineCap = StrokeCap.Butt, @@ -84,7 +92,77 @@ class Icons { } } @Composable - fun VideoGame(): ImageVector { + fun vSync(): ImageVector { + val primaryColor = MaterialTheme.colorScheme.primary + return remember { + ImageVector.Builder( + name = "60fps", + defaultWidth = 40.0.dp, + defaultHeight = 40.0.dp, + viewportWidth = 40.0f, + viewportHeight = 40.0f + ).apply { + path( + fill = SolidColor(Color.Black.copy(alpha = 0.5f)), + stroke = SolidColor(primaryColor), + fillAlpha = 1f, + strokeAlpha = 1f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1f, + pathFillType = PathFillType.NonZero + ) { + moveTo(7.292f, 31.458f) + quadToRelative(-1.542f, 0f, -2.625f, -1.041f) + quadToRelative(-1.084f, -1.042f, -1.084f, -2.625f) + verticalLineTo(12.208f) + quadToRelative(0f, -1.583f, 1.084f, -2.625f) + quadTo(5.75f, 8.542f, 7.292f, 8.542f) + horizontalLineTo(14f) + quadToRelative(0.75f, 0f, 1.292f, 0.541f) + quadToRelative(0.541f, 0.542f, 0.541f, 1.292f) + reflectiveQuadToRelative(-0.541f, 1.292f) + quadToRelative(-0.542f, 0.541f, -1.292f, 0.541f) + horizontalLineTo(7.208f) + verticalLineToRelative(5.084f) + horizontalLineToRelative(6.709f) + quadToRelative(1.541f, 0f, 2.583f, 1.041f) + quadToRelative(1.042f, 1.042f, 1.042f, 2.625f) + verticalLineToRelative(6.834f) + quadToRelative(0f, 1.583f, -1.042f, 2.625f) + quadToRelative(-1.042f, 1.041f, -2.583f, 1.041f) + close() + moveToRelative(-0.084f, -10.5f) + verticalLineToRelative(6.834f) + horizontalLineToRelative(6.709f) + verticalLineToRelative(-6.834f) + close() + moveToRelative(17.125f, 6.834f) + horizontalLineToRelative(8.459f) + verticalLineTo(12.208f) + horizontalLineToRelative(-8.459f) + verticalLineToRelative(15.584f) + close() + moveToRelative(0f, 3.666f) + quadToRelative(-1.541f, 0f, -2.583f, -1.041f) + quadToRelative(-1.042f, -1.042f, -1.042f, -2.625f) + verticalLineTo(12.208f) + quadToRelative(0f, -1.583f, 1.042f, -2.625f) + quadToRelative(1.042f, -1.041f, 2.583f, -1.041f) + horizontalLineToRelative(8.459f) + quadToRelative(1.541f, 0f, 2.583f, 1.041f) + quadToRelative(1.042f, 1.042f, 1.042f, 2.625f) + verticalLineToRelative(15.584f) + quadToRelative(0f, 1.583f, -1.042f, 2.625f) + quadToRelative(-1.042f, 1.041f, -2.583f, 1.041f) + close() + } + }.build() + } + } + @Composable + fun videoGame(): ImageVector { val primaryColor = MaterialTheme.colorScheme.primary return remember { ImageVector.Builder( @@ -96,8 +174,8 @@ class Icons { ).apply { path( fill = SolidColor(Color.Black.copy(alpha = 0.5f)), - fillAlpha = 1f, stroke = SolidColor(primaryColor), + fillAlpha = 1f, strokeAlpha = 1f, strokeLineWidth = 1.0f, strokeLineCap = StrokeCap.Butt, @@ -179,4 +257,16 @@ class Icons { } } } -} \ No newline at end of file +} + +@Preview +@Composable +fun Preview(){ + IconButton(modifier = Modifier.padding(4.dp), onClick = { + }) { + Icon( + imageVector = CssGgIcons.Games, + contentDescription = "Open Panel" + ) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 178ee43b6..d9817f69a 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -1,39 +1,23 @@ package org.ryujinx.android -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.ActivityInfo -import android.media.AudioDeviceInfo -import android.media.AudioManager import android.os.Build import android.os.Bundle import android.os.Environment -import android.view.KeyEvent -import android.view.MotionEvent import android.view.WindowManager import androidx.activity.ComponentActivity -import androidx.activity.addCallback import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat import com.anggrayudi.storage.SimpleStorageHelper import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.viewmodels.MainViewModel -import org.ryujinx.android.viewmodels.VulkanDriverViewModel -import org.ryujinx.android.views.HomeViews import org.ryujinx.android.views.MainView -import java.io.File class MainActivity : ComponentActivity() { - var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this) private var _isInit: Boolean = false var storageHelper: SimpleStorageHelper? = null companion object { @@ -61,60 +45,7 @@ class MainActivity : ComponentActivity() { } external fun getRenderingThreadId() : Long - external fun initVm() - - fun setFullScreen(fullscreen: Boolean) { - requestedOrientation = - if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER - - val insets = WindowCompat.getInsetsController(window, window.decorView) - - insets.apply { - if (fullscreen) { - insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) - insets.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } else { - insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) - insets.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_DEFAULT - } - } - } - - private fun getAudioDevice () : Int { - val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - - return if (devices.isEmpty()) - 0 - else { - val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET } - if(earPiece != null) - return earPiece.id - if(speaker != null) - return speaker.id - devices.first().id - } - } - - @SuppressLint("RestrictedApi") - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - event?.apply { - if(physicalControllerManager.onKeyEvent(this)) - return true; - } - return super.dispatchKeyEvent(event) - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - ev?.apply { - physicalControllerManager.onMotionEvent(this) - } - return super.dispatchGenericMotionEvent(ev) - } + private external fun initVm() private fun initialize() { if (_isInit) @@ -150,15 +81,6 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - /*Box { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - GameHost(context) - } - ) - controller.Compose(lifecycleScope, lifecycle) - }*/ MainView.Main(mainViewModel = this) } } @@ -176,16 +98,3 @@ class MainActivity : ComponentActivity() { storageHelper?.onRestoreInstanceState(savedInstanceState) } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - RyujinxAndroidTheme { - HomeViews.Home() - } -} \ No newline at end of file diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt index 262b551c3..e4dddff01 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeGraphicsInterop.kt @@ -1,9 +1,7 @@ package org.ryujinx.android -import android.view.Surface - class NativeGraphicsInterop { var VkCreateSurface: Long = 0 var SurfaceHandle: Long = 0 var VkRequiredExtensions: Array? = null -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt index 4b52c2862..f37114ad7 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -9,12 +9,14 @@ class NativeHelpers { System.loadLibrary("ryujinxjni") } } - external fun releaseNativeWindow(window:Long) : Unit + external fun releaseNativeWindow(window:Long) external fun createSurface(vkInstance:Long, window:Long) : Long external fun getCreateSurfacePtr() : Long external fun getNativeWindow(surface:Surface) : Long - external fun attachCurrentThread() : Unit - external fun detachCurrentThread() : Unit + external fun attachCurrentThread() + external fun detachCurrentThread() external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long -} \ No newline at end of file + + external fun setTurboMode(enable: Boolean) +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt index b675854e5..0b1cdbfdd 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PerformanceManager.kt @@ -4,10 +4,10 @@ import android.os.Build import android.os.PerformanceHintManager import androidx.annotation.RequiresApi -class PerformanceManager(val performanceHintManager: PerformanceHintManager) { +class PerformanceManager(private val performanceHintManager: PerformanceHintManager) { private var _isEnabled: Boolean = false private var renderingSession: PerformanceHintManager.Session? = null - val DEFAULT_TARGET_NS = 16666666L + private val DEFAULT_TARGET_NS = 16666666L @RequiresApi(Build.VERSION_CODES.S) fun initializeRenderingSession(threadId : Long){ @@ -46,4 +46,4 @@ class PerformanceManager(val performanceHintManager: PerformanceHintManager) { this.reportActualWorkDuration(effectiveTime) } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt index 7191c883e..9913956bd 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt @@ -3,13 +3,13 @@ package org.ryujinx.android import android.view.KeyEvent import android.view.MotionEvent -class PhysicalControllerManager(val activity: MainActivity) { +class PhysicalControllerManager(val activity: GameActivity) { private var controllerId: Int = -1 private var ryujinxNative: RyujinxNative = RyujinxNative() fun onKeyEvent(event: KeyEvent) : Boolean{ if(controllerId != -1) { - val id = GetGamePadButtonInputId(event.keyCode) + val id = getGamePadButtonInputId(event.keyCode) if(id != GamePadButtonInputId.None) { when (event.action) { @@ -45,7 +45,7 @@ class PhysicalControllerManager(val activity: MainActivity) { controllerId = ryujinxNative.inputConnectGamepad(0) } - fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId { + private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId { return when (keycode) { KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A @@ -66,4 +66,4 @@ class PhysicalControllerManager(val activity: MainActivity) { else -> GamePadButtonInputId.None } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index c89259147..682050602 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -35,21 +35,21 @@ class RyujinxNative { external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo external fun deviceGetGameInfoFromPath(path: String): GameInfo external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean - external fun graphicsRendererSetSize(width: Int, height: Int): Unit - external fun graphicsRendererSetVsync(enabled: Boolean): Unit - external fun graphicsRendererRunLoop(): Unit - external fun inputInitialize(width: Int, height: Int): Unit - external fun inputSetClientSize(width: Int, height: Int): Unit - external fun inputSetTouchPoint(x: Int, y: Int): Unit - external fun inputReleaseTouchPoint(): Unit - external fun inputUpdate(): Unit - external fun inputSetButtonPressed(button: Int, id: Int): Unit - external fun inputSetButtonReleased(button: Int, id: Int): Unit + external fun graphicsRendererSetSize(width: Int, height: Int) + external fun graphicsRendererSetVsync(enabled: Boolean) + external fun graphicsRendererRunLoop() + external fun inputInitialize(width: Int, height: Int) + external fun inputSetClientSize(width: Int, height: Int) + external fun inputSetTouchPoint(x: Int, y: Int) + external fun inputReleaseTouchPoint() + external fun inputUpdate() + external fun inputSetButtonPressed(button: Int, id: Int) + external fun inputSetButtonReleased(button: Int, id: Int) external fun inputConnectGamepad(index: Int): Int - external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int): Unit + external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int) external fun graphicsSetSurface(surface: Long) external fun deviceCloseEmulation() external fun deviceSignalEmulationClose() external fun deviceGetDlcTitleId(path: String, ncaPath: String) : String external fun deviceGetDlcContentList(path: String, titleId: Long) : Array -} \ No newline at end of file +} 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 0a29d717e..a06edcaff 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 @@ -2,6 +2,7 @@ package org.ryujinx.android.viewmodels import android.content.Context import android.net.Uri +import android.os.ParcelFileDescriptor import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.file.extension import org.ryujinx.android.Helpers @@ -9,6 +10,7 @@ import org.ryujinx.android.RyujinxNative class GameModel(var file: DocumentFile, val context: Context) { + private var descriptor: ParcelFileDescriptor? = null var fileName: String? var fileSize = 0.0 var titleName: String? = null @@ -37,7 +39,18 @@ class GameModel(var file: DocumentFile, val context: Context) { return uri.path } - fun getIsXci() : Boolean { + fun open() : Int { + descriptor = context.contentResolver.openFileDescriptor(file.uri, "rw") + + return descriptor?.fd ?: 0 + } + + fun close() { + descriptor?.close() + descriptor = null + } + + fun isXci() : Boolean { return file.extension == "xci" } } @@ -49,4 +62,4 @@ class GameInfo { var Developer: String? = null var Version: String? = null var IconCache: String? = null -} \ No newline at end of file +} 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 e8361e455..c6c157f8d 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 @@ -2,10 +2,12 @@ package org.ryujinx.android.viewmodels import android.annotation.SuppressLint import android.content.Context +import android.content.Intent import android.os.Build import android.os.PerformanceHintManager import androidx.compose.runtime.MutableState import androidx.navigation.NavHostController +import org.ryujinx.android.GameActivity import org.ryujinx.android.GameController import org.ryujinx.android.GameHost import org.ryujinx.android.GraphicsConfiguration @@ -13,6 +15,7 @@ import org.ryujinx.android.MainActivity import org.ryujinx.android.NativeGraphicsInterop import org.ryujinx.android.NativeHelpers import org.ryujinx.android.PerformanceManager +import org.ryujinx.android.PhysicalControllerManager import org.ryujinx.android.RegionCode import org.ryujinx.android.RyujinxNative import org.ryujinx.android.SystemLanguage @@ -20,6 +23,8 @@ import java.io.File @SuppressLint("WrongConstant") class MainViewModel(val activity: MainActivity) { + var physicalControllerManager: PhysicalControllerManager? = null + var gameModel: GameModel? = null var gameHost: GameHost? = null var controller: GameController? = null var performanceManager: PerformanceManager? = null @@ -43,18 +48,17 @@ class MainViewModel(val activity: MainActivity) { RyujinxNative().deviceSignalEmulationClose() gameHost?.close() RyujinxNative().deviceCloseEmulation() - goBack() - activity.setFullScreen(false) - } - - fun goBack(){ - navController?.popBackStack() } fun loadGame(game:GameModel) : Boolean { - var nativeRyujinx = RyujinxNative() + val nativeRyujinx = RyujinxNative() - val path = game.getPath() ?: return false + val descriptor = game.open() + + if(descriptor == 0) + return false + + gameModel = game val settings = QuickSettings(activity) @@ -62,42 +66,45 @@ class MainViewModel(val activity: MainActivity) { EnableShaderCache = settings.enableShaderCache EnableTextureRecompression = settings.enableTextureRecompression ResScale = settings.resScale + BackendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal }) if(!success) return false val nativeHelpers = NativeHelpers() - var nativeInterop = NativeGraphicsInterop() - nativeInterop!!.VkRequiredExtensions = arrayOf( + val nativeInterop = NativeGraphicsInterop() + nativeInterop.VkRequiredExtensions = arrayOf( "VK_KHR_surface", "VK_KHR_android_surface" ) - nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() - nativeInterop!!.SurfaceHandle = 0 + nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr() + nativeInterop.SurfaceHandle = 0 - var driverViewModel = VulkanDriverViewModel(activity); - var drivers = driverViewModel.getAvailableDrivers() + val driverViewModel = VulkanDriverViewModel(activity) + val drivers = driverViewModel.getAvailableDrivers() - var driverHandle = 0L; + var driverHandle = 0L if (driverViewModel.selected.isNotEmpty()) { - var metaData = drivers.find { it.driverPath == driverViewModel.selected } + val metaData = drivers.find { it.driverPath == driverViewModel.selected } metaData?.apply { - var privatePath = activity.filesDir; - var privateDriverPath = privatePath.canonicalPath + "/driver/" + val privatePath = activity.filesDir + val privateDriverPath = privatePath.canonicalPath + "/driver/" val pD = File(privateDriverPath) if (pD.exists()) pD.deleteRecursively() pD.mkdirs() - var driver = File(driverViewModel.selected) - var parent = driver.parentFile - for (file in parent.walkTopDown()) { - if (file.absolutePath == parent.absolutePath) - continue - file.copyTo(File(privateDriverPath + file.name), true) + val driver = File(driverViewModel.selected) + val parent = driver.parentFile + if (parent != null) { + for (file in parent.walkTopDown()) { + if (file.absolutePath == parent.absolutePath) + continue + file.copyTo(File(privateDriverPath + file.name), true) + } } driverHandle = NativeHelpers().loadDriver( @@ -110,7 +117,7 @@ class MainViewModel(val activity: MainActivity) { } success = nativeRyujinx.graphicsInitializeRenderer( - nativeInterop!!.VkRequiredExtensions!!, + nativeInterop.VkRequiredExtensions!!, driverHandle ) if(!success) @@ -131,7 +138,7 @@ class MainViewModel(val activity: MainActivity) { if(!success) return false - success = nativeRyujinx.deviceLoad(path) + success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci()) if(!success) return false @@ -169,6 +176,8 @@ class MainViewModel(val activity: MainActivity) { this.controller = controller } - fun backCalled() { + fun navigateToGame() { + val intent = Intent(activity, GameActivity::class.java) + activity.startActivity(intent) } -} \ No newline at end of file +} 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 de1d6b320..18300f52b 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 @@ -1,10 +1,10 @@ package org.ryujinx.android.viewmodels +import android.app.Activity import android.content.SharedPreferences import androidx.preference.PreferenceManager -import org.ryujinx.android.MainActivity -class QuickSettings(val activity: MainActivity) { +class QuickSettings(val activity: Activity) { var ignoreMissingServices: Boolean var enablePtc: Boolean var enableDocked: Boolean @@ -30,4 +30,4 @@ class QuickSettings(val activity: MainActivity) { resScale = sharedPref.getFloat("resScale", 1f) useVirtualController = sharedPref.getBoolean("useVirtualController", true) } -} \ No newline at end of file +} 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 4d9b56fea..5a8e44167 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 @@ -21,6 +21,7 @@ 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.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog @@ -47,7 +48,6 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -68,6 +68,7 @@ import org.ryujinx.android.R import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.HomeViewModel import java.io.File +import java.util.Locale import kotlin.math.roundToInt class HomeViews { @@ -76,7 +77,11 @@ class HomeViews { @OptIn(ExperimentalMaterial3Api::class) @Composable - fun MainTopBar(navController: NavHostController) { + fun MainTopBar( + navController: NavHostController, + query: MutableState, + refresh: MutableState + ) { val topBarSize = remember { mutableStateOf(0) } @@ -87,15 +92,17 @@ class HomeViews { TopAppBar( modifier = Modifier .zIndex(1f) - .padding(top = 16.dp) + .padding(top = 8.dp) .onSizeChanged { topBarSize.value = it.height }, title = { DockedSearchBar( shape = SearchBarDefaults.inputFieldShape, - query = "", - onQueryChange = {}, + query = query.value, + onQueryChange = { + query.value = it + }, onSearch = {}, active = false, onActiveChange = {}, @@ -113,6 +120,16 @@ class HomeViews { } }, actions = { + IconButton( + onClick = { + refresh.value = true + } + ) { + Icon( + Icons.Filled.Refresh, + contentDescription = "Refresh" + ) + } IconButton( onClick = { showOptionsPopup.value = true @@ -126,15 +143,17 @@ class HomeViews { } ) Box { - if(showOptionsPopup.value) - { + if (showOptionsPopup.value) { AlertDialog( - modifier = Modifier.padding(top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp, - start = 16.dp, end = 16.dp), + modifier = Modifier.padding( + top = (topBarSize.value / Resources.getSystem().displayMetrics.density + 10).dp, + start = 16.dp, end = 16.dp + ), onDismissRequest = { showOptionsPopup.value = false }) { - val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider + val dialogWindowProvider = + LocalView.current.parent as DialogWindowProvider dialogWindowProvider.window.setGravity(Gravity.TOP) Surface( modifier = Modifier @@ -145,19 +164,23 @@ class HomeViews { tonalElevation = AlertDialogDefaults.TonalElevation ) { Column { - TextButton(onClick = { - navController.navigate("settings") - }, modifier = Modifier - .fillMaxWidth() - .align(Alignment.Start), + TextButton( + onClick = { + navController.navigate("settings") + }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Start), ) { Icon( Icons.Filled.Settings, contentDescription = "Settings" ) - Text(text = "Settings", modifier = Modifier - .padding(16.dp) - .align(Alignment.CenterVertically)) + Text( + text = "Settings", modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterVertically) + ) } } } @@ -171,15 +194,19 @@ class HomeViews { @Composable fun Home(viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null) { val sheetState = rememberModalBottomSheetState() - val scope = rememberCoroutineScope() val showBottomSheet = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) } - + val query = remember { + mutableStateOf("") + } + val refresh = remember { + mutableStateOf(true) + } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { navController?.apply { - MainTopBar(navController) + MainTopBar(navController, query, refresh) } }, floatingActionButtonPosition = FabPosition.End, @@ -200,11 +227,19 @@ class HomeViews { val list = remember { mutableStateListOf() } - viewModel.setViewList(list) + + + if(refresh.value) { + viewModel.setViewList(list) + refresh.value = false + } LazyColumn(Modifier.fillMaxSize()) { items(list) { it.titleName?.apply { - if (this.isNotEmpty()) + if (this.isNotEmpty() && (query.value.trim().isEmpty() || this.lowercase( + Locale.getDefault() + ) + .contains(query.value))) GameItem(it, viewModel, showBottomSheet, showLoading) } } @@ -308,7 +343,7 @@ class HomeViews { ) { Column(modifier = Modifier.padding(16.dp)) { Icon( - imageVector = org.ryujinx.android.Icons.Download(), + imageVector = org.ryujinx.android.Icons.download(), contentDescription = "Game Dlc", tint = Color.Green, modifier = Modifier @@ -352,11 +387,10 @@ class HomeViews { viewModel.mainViewModel?.loadGame(gameModel) ?: false if (success) { launchOnUiThread { - viewModel.mainViewModel?.activity?.setFullScreen( - true - ) - viewModel.mainViewModel?.navController?.navigate("game") + viewModel.mainViewModel?.navigateToGame() } + } else { + gameModel.close() } showLoading.value = false } @@ -421,4 +455,4 @@ class HomeViews { fun HomePreview() { Home() } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt index b37699768..7c5c988e1 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/MainView.kt @@ -1,50 +1,11 @@ package org.ryujinx.android.views -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -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.graphics.PathFillType -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.StrokeJoin -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.path -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import org.ryujinx.android.GameController -import org.ryujinx.android.GameHost -import org.ryujinx.android.Icons -import org.ryujinx.android.RyujinxNative import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.SettingsViewModel -import kotlin.math.roundToInt class MainView { companion object { @@ -55,7 +16,6 @@ class MainView { NavHost(navController = navController, startDestination = "home") { composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) } - composable("game") { GameView(mainViewModel) } composable("settings") { SettingViews.Main( SettingsViewModel( @@ -66,165 +26,5 @@ class MainView { } } } - - @Composable - fun GameView(mainViewModel: MainViewModel) { - Box(modifier = Modifier.fillMaxSize()) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - GameHost(context, mainViewModel) - } - ) - GameOverlay(mainViewModel) - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun GameOverlay(mainViewModel: MainViewModel) { - Box(modifier = Modifier.fillMaxSize()) { - GameStats(mainViewModel) - - val ryujinxNative = RyujinxNative() - - // touch surface - Surface(color = Color.Transparent, modifier = Modifier - .fillMaxSize() - .padding(0.dp) - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - Thread.sleep(2) - val event = awaitPointerEvent() - - val change = event - .component1() - .firstOrNull() - change?.apply { - val position = this.position - - when (event.type) { - PointerEventType.Press -> { - ryujinxNative.inputSetTouchPoint( - position.x.roundToInt(), - position.y.roundToInt() - ) - } - - PointerEventType.Release -> { - ryujinxNative.inputReleaseTouchPoint() - - } - - PointerEventType.Move -> { - ryujinxNative.inputSetTouchPoint( - position.x.roundToInt(), - position.y.roundToInt() - ) - - } - } - } - } - } - }) { - } - GameController.Compose(mainViewModel) - Row( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(8.dp) - ) { - IconButton(modifier = Modifier.padding(4.dp), onClick = { - mainViewModel.controller?.setVisible(!mainViewModel.controller!!.isVisible) - }) { - Icon( - imageVector = Icons.VideoGame(), - contentDescription = "Toggle Virtual Pad" - ) - } - } - - var showBackNotice = remember { - mutableStateOf(false) - } - - BackHandler { - showBackNotice.value = true - } - - if (showBackNotice.value) { - AlertDialog(onDismissRequest = { showBackNotice.value = false }) { - Column { - Surface( - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight(), - shape = MaterialTheme.shapes.large, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text(text = "Are you sure you want to exit the game?") - Text(text = "All unsaved data will be lost!") - } - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Button(onClick = { - mainViewModel.closeGame() - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Exit Game") - } - - Button(onClick = { - showBackNotice.value = false - }, modifier = Modifier.padding(16.dp)) { - Text(text = "Dismiss") - } - } - } - } - } - } - } - } - } - @Composable - fun GameStats(mainViewModel: MainViewModel) { - val fifo = remember { - mutableStateOf(0.0) - } - val gameFps = remember { - mutableStateOf(0.0) - } - val gameTime = remember { - mutableStateOf(0.0) - } - - Surface( - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.surface.copy(0.4f) - ) { - Column { - var gameTimeVal = 0.0 - if (!gameTime.value.isInfinite()) - gameTimeVal = gameTime.value - Text(text = "${String.format("%.3f", fifo.value)} %") - Text(text = "${String.format("%.3f", gameFps.value)} FPS") - Text(text = "${String.format("%.3f", gameTimeVal)} ms") - } - } - - mainViewModel.setStatStates(fifo, gameFps, gameTime) - } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/settings.gradle b/src/RyujinxAndroid/settings.gradle index 0c8087171..766b6e6f2 100644 --- a/src/RyujinxAndroid/settings.gradle +++ b/src/RyujinxAndroid/settings.gradle @@ -4,6 +4,7 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven { url 'https://jitpack.io' } + maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } } } dependencyResolutionManagement { @@ -12,6 +13,7 @@ dependencyResolutionManagement { google() mavenCentral() maven { url 'https://jitpack.io' } + maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } } } rootProject.name = "RyujinxAndroid"