forked from MeloNX/MeloNX
separate game loading from surface creation
This commit is contained in:
parent
398fb78a31
commit
a050e5c6c0
@ -218,7 +218,6 @@ namespace LibRyujinx
|
|||||||
public unsafe static JBoolean JniInitializeGraphicsRendererNative(JEnvRef jEnv,
|
public unsafe static JBoolean JniInitializeGraphicsRendererNative(JEnvRef jEnv,
|
||||||
JObjectLocalRef jObj,
|
JObjectLocalRef jObj,
|
||||||
JArrayLocalRef extensionsArray,
|
JArrayLocalRef extensionsArray,
|
||||||
JLong surfacePtr,
|
|
||||||
JLong driverHandle)
|
JLong driverHandle)
|
||||||
{
|
{
|
||||||
if (Renderer != null)
|
if (Renderer != null)
|
||||||
@ -254,10 +253,6 @@ namespace LibRyujinx
|
|||||||
extensions.Add(GetString(jEnv, ext));
|
extensions.Add(GetString(jEnv, ext));
|
||||||
}
|
}
|
||||||
|
|
||||||
_surfaceEvent.Set();
|
|
||||||
|
|
||||||
_surfacePtr = surfacePtr;
|
|
||||||
|
|
||||||
if((long)driverHandle != 0)
|
if((long)driverHandle != 0)
|
||||||
{
|
{
|
||||||
VulkanLoader = new VulkanLoader((IntPtr)(long)driverHandle);
|
VulkanLoader = new VulkanLoader((IntPtr)(long)driverHandle);
|
||||||
|
@ -59,9 +59,6 @@ android {
|
|||||||
resources {
|
resources {
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||||
}
|
}
|
||||||
jniLibs {
|
|
||||||
keepDebugSymbols += '**/libryujinx.so'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
|
@ -37,11 +37,9 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||||
val isStarted = _isStarted
|
|
||||||
|
|
||||||
start(holder)
|
start(holder)
|
||||||
|
|
||||||
if(isStarted && (_width != width || _height != height))
|
if(_width != width || _height != height)
|
||||||
{
|
{
|
||||||
val nativeHelpers = NativeHelpers()
|
val nativeHelpers = NativeHelpers()
|
||||||
val window = nativeHelpers.getNativeWindow(holder.surface)
|
val window = nativeHelpers.getNativeWindow(holder.surface)
|
||||||
@ -62,85 +60,14 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun start(surfaceHolder: SurfaceHolder) {
|
private fun start(surfaceHolder: SurfaceHolder) {
|
||||||
val game = gameModel ?: return
|
|
||||||
val path = game.getPath() ?: return
|
|
||||||
if (_isStarted)
|
|
||||||
return
|
|
||||||
|
|
||||||
var surface = surfaceHolder.surface
|
if(_isStarted)
|
||||||
|
return;
|
||||||
val settings = QuickSettings(mainViewModel.activity)
|
|
||||||
|
|
||||||
var success = _nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply {
|
|
||||||
EnableShaderCache = settings.enableShaderCache
|
|
||||||
EnableTextureRecompression = settings.enableTextureRecompression
|
|
||||||
ResScale = settings.resScale
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
val nativeHelpers = NativeHelpers()
|
|
||||||
val window = nativeHelpers.getNativeWindow(surfaceHolder.surface)
|
|
||||||
nativeInterop = NativeGraphicsInterop()
|
|
||||||
nativeInterop!!.VkRequiredExtensions = arrayOf(
|
|
||||||
"VK_KHR_surface", "VK_KHR_android_surface"
|
|
||||||
)
|
|
||||||
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
|
|
||||||
nativeInterop!!.SurfaceHandle = window
|
|
||||||
|
|
||||||
var driverViewModel = VulkanDriverViewModel(mainViewModel.activity);
|
|
||||||
var drivers = driverViewModel.getAvailableDrivers()
|
|
||||||
|
|
||||||
var driverHandle = 0L;
|
|
||||||
|
|
||||||
if(driverViewModel.selected.isNotEmpty()) {
|
|
||||||
var metaData = drivers.find { it.driverPath == driverViewModel.selected }
|
|
||||||
|
|
||||||
metaData?.apply {
|
|
||||||
var privatePath = mainViewModel.activity.filesDir;
|
|
||||||
var 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
driverHandle = NativeHelpers().loadDriver(mainViewModel.activity.applicationInfo.nativeLibraryDir!! + "/", privateDriverPath, this.libraryName)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
success = _nativeRyujinx.graphicsInitializeRenderer(
|
|
||||||
nativeInterop!!.VkRequiredExtensions!!,
|
|
||||||
window,
|
|
||||||
driverHandle
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
success = _nativeRyujinx.deviceInitialize(
|
|
||||||
settings.isHostMapped,
|
|
||||||
settings.useNce,
|
|
||||||
SystemLanguage.AmericanEnglish.ordinal,
|
|
||||||
RegionCode.USA.ordinal,
|
|
||||||
settings.enableVsync,
|
|
||||||
settings.enableDocked,
|
|
||||||
settings.enablePtc,
|
|
||||||
false,
|
|
||||||
"UTC",
|
|
||||||
settings.ignoreMissingServices
|
|
||||||
)
|
|
||||||
|
|
||||||
success = _nativeRyujinx.deviceLoad(path)
|
|
||||||
|
|
||||||
_nativeRyujinx.inputInitialize(width, height)
|
_nativeRyujinx.inputInitialize(width, height)
|
||||||
|
|
||||||
|
val settings = QuickSettings(mainViewModel.activity)
|
||||||
|
|
||||||
if(!settings.useVirtualController){
|
if(!settings.useVirtualController){
|
||||||
mainViewModel.controller?.setVisible(false)
|
mainViewModel.controller?.setVisible(false)
|
||||||
}
|
}
|
||||||
@ -158,7 +85,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
|
|||||||
_guestThread = thread(start = true) {
|
_guestThread = thread(start = true) {
|
||||||
runGame()
|
runGame()
|
||||||
}
|
}
|
||||||
_isStarted = success
|
_isStarted = true
|
||||||
|
|
||||||
_updateThread = thread(start = true) {
|
_updateThread = thread(start = true) {
|
||||||
var c = 0
|
var c = 0
|
||||||
|
@ -25,7 +25,6 @@ class RyujinxNative {
|
|||||||
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
||||||
external fun graphicsInitializeRenderer(
|
external fun graphicsInitializeRenderer(
|
||||||
extensions: Array<String>,
|
extensions: Array<String>,
|
||||||
surface: Long,
|
|
||||||
driver: Long
|
driver: Long
|
||||||
): Boolean
|
): Boolean
|
||||||
|
|
||||||
@ -48,5 +47,5 @@ class RyujinxNative {
|
|||||||
external fun inputSetButtonReleased(button: Int, id: Int): Unit
|
external fun inputSetButtonReleased(button: Int, id: Int): Unit
|
||||||
external fun inputConnectGamepad(index: Int): 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): Unit
|
||||||
external fun graphicsSetSurface(surface: Long): String
|
external fun graphicsSetSurface(surface: Long)
|
||||||
}
|
}
|
@ -6,10 +6,21 @@ import android.os.Build
|
|||||||
import android.os.PerformanceHintManager
|
import android.os.PerformanceHintManager
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.ryujinx.android.GameController
|
import org.ryujinx.android.GameController
|
||||||
import org.ryujinx.android.GameHost
|
import org.ryujinx.android.GameHost
|
||||||
|
import org.ryujinx.android.GraphicsConfiguration
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import org.ryujinx.android.NativeGraphicsInterop
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.PerformanceManager
|
import org.ryujinx.android.PerformanceManager
|
||||||
|
import org.ryujinx.android.RegionCode
|
||||||
|
import org.ryujinx.android.RyujinxNative
|
||||||
|
import org.ryujinx.android.SystemLanguage
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
class MainViewModel(val activity: MainActivity) {
|
class MainViewModel(val activity: MainActivity) {
|
||||||
@ -19,7 +30,7 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
private var gameTimeState: MutableState<Double>? = null
|
private var gameTimeState: MutableState<Double>? = null
|
||||||
private var gameFpsState: MutableState<Double>? = null
|
private var gameFpsState: MutableState<Double>? = null
|
||||||
private var fifoState: MutableState<Double>? = null
|
private var fifoState: MutableState<Double>? = null
|
||||||
private var navController : NavHostController? = null
|
var navController : NavHostController? = null
|
||||||
|
|
||||||
var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
|
var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
|
||||||
|
|
||||||
@ -31,15 +42,96 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadGame(game:GameModel) {
|
suspend fun loadGame(game:GameModel) : Boolean {
|
||||||
val controller = navController?: return
|
|
||||||
activity.setFullScreen()
|
|
||||||
GameHost.gameModel = game
|
GameHost.gameModel = game
|
||||||
controller.navigate("game")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNavController(controller: NavHostController) {
|
var nativeRyujinx = RyujinxNative()
|
||||||
navController = controller
|
|
||||||
|
val path = game.getPath() ?: return false
|
||||||
|
|
||||||
|
val settings = QuickSettings(activity)
|
||||||
|
|
||||||
|
var success = nativeRyujinx.graphicsInitialize(GraphicsConfiguration().apply {
|
||||||
|
EnableShaderCache = settings.enableShaderCache
|
||||||
|
EnableTextureRecompression = settings.enableTextureRecompression
|
||||||
|
ResScale = settings.resScale
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!success)
|
||||||
|
return false
|
||||||
|
|
||||||
|
val nativeHelpers = NativeHelpers()
|
||||||
|
var nativeInterop = NativeGraphicsInterop()
|
||||||
|
nativeInterop!!.VkRequiredExtensions = arrayOf(
|
||||||
|
"VK_KHR_surface", "VK_KHR_android_surface"
|
||||||
|
)
|
||||||
|
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
|
||||||
|
nativeInterop!!.SurfaceHandle = 0
|
||||||
|
|
||||||
|
var driverViewModel = VulkanDriverViewModel(activity);
|
||||||
|
var drivers = driverViewModel.getAvailableDrivers()
|
||||||
|
|
||||||
|
var driverHandle = 0L;
|
||||||
|
|
||||||
|
if (driverViewModel.selected.isNotEmpty()) {
|
||||||
|
var metaData = drivers.find { it.driverPath == driverViewModel.selected }
|
||||||
|
|
||||||
|
metaData?.apply {
|
||||||
|
var privatePath = activity.filesDir;
|
||||||
|
var 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
driverHandle = NativeHelpers().loadDriver(
|
||||||
|
activity.applicationInfo.nativeLibraryDir!! + "/",
|
||||||
|
privateDriverPath,
|
||||||
|
this.libraryName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
success = nativeRyujinx.graphicsInitializeRenderer(
|
||||||
|
nativeInterop!!.VkRequiredExtensions!!,
|
||||||
|
driverHandle
|
||||||
|
)
|
||||||
|
if(!success)
|
||||||
|
return false
|
||||||
|
|
||||||
|
success = nativeRyujinx.deviceInitialize(
|
||||||
|
settings.isHostMapped,
|
||||||
|
settings.useNce,
|
||||||
|
SystemLanguage.AmericanEnglish.ordinal,
|
||||||
|
RegionCode.USA.ordinal,
|
||||||
|
settings.enableVsync,
|
||||||
|
settings.enableDocked,
|
||||||
|
settings.enablePtc,
|
||||||
|
false,
|
||||||
|
"UTC",
|
||||||
|
settings.ignoreMissingServices
|
||||||
|
)
|
||||||
|
if(!success)
|
||||||
|
return false
|
||||||
|
|
||||||
|
success = nativeRyujinx.deviceLoad(path)
|
||||||
|
|
||||||
|
if(!success)
|
||||||
|
return false
|
||||||
|
|
||||||
|
activity.physicalControllerManager.connect()
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStatStates(
|
fun setStatStates(
|
||||||
|
@ -32,6 +32,7 @@ import androidx.compose.material3.FabPosition
|
|||||||
import androidx.compose.material3.FloatingActionButton
|
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.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@ -55,10 +56,14 @@ import androidx.compose.ui.platform.LocalView
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
import org.ryujinx.android.R
|
import org.ryujinx.android.R
|
||||||
import org.ryujinx.android.viewmodels.GameModel
|
import org.ryujinx.android.viewmodels.GameModel
|
||||||
@ -143,14 +148,16 @@ class HomeViews {
|
|||||||
Column {
|
Column {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
navController.navigate("settings")
|
navController.navigate("settings")
|
||||||
}, modifier = Modifier.fillMaxWidth()
|
}, modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.align(Alignment.Start),
|
.align(Alignment.Start),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Settings,
|
Icons.Filled.Settings,
|
||||||
contentDescription = "Settings"
|
contentDescription = "Settings"
|
||||||
)
|
)
|
||||||
Text(text = "Settings", modifier = Modifier.padding(16.dp)
|
Text(text = "Settings", modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
.align(Alignment.CenterVertically))
|
.align(Alignment.CenterVertically))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,6 +174,7 @@ class HomeViews {
|
|||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val showBottomSheet = remember { mutableStateOf(false) }
|
val showBottomSheet = remember { mutableStateOf(false) }
|
||||||
|
val showLoading = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -198,11 +206,30 @@ class HomeViews {
|
|||||||
items(list) {
|
items(list) {
|
||||||
it.titleName?.apply {
|
it.titleName?.apply {
|
||||||
if (this.isNotEmpty())
|
if (this.isNotEmpty())
|
||||||
GameItem(it, viewModel, showBottomSheet)
|
GameItem(it, viewModel, showBottomSheet, showLoading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(showLoading.value){
|
||||||
|
AlertDialog(onDismissRequest = { }) {
|
||||||
|
Card(modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth()) {
|
||||||
|
Text(text = "Loading")
|
||||||
|
LinearProgressIndicator(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(showBottomSheet.value) {
|
if(showBottomSheet.value) {
|
||||||
ModalBottomSheet(onDismissRequest = {
|
ModalBottomSheet(onDismissRequest = {
|
||||||
@ -264,15 +291,32 @@ class HomeViews {
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GameItem(gameModel: GameModel, viewModel: HomeViewModel, showSheet : MutableState<Boolean>) {
|
fun GameItem(
|
||||||
Card(shape = MaterialTheme.shapes.medium,
|
gameModel: GameModel,
|
||||||
|
viewModel: HomeViewModel,
|
||||||
|
showSheet: MutableState<Boolean>,
|
||||||
|
showLoading: MutableState<Boolean>
|
||||||
|
) {
|
||||||
|
Surface(shape = MaterialTheme.shapes.medium,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
||||||
viewModel.mainViewModel?.loadGame(gameModel)
|
runBlocking {
|
||||||
|
launch {
|
||||||
|
showLoading.value = true
|
||||||
|
val success = viewModel.mainViewModel?.loadGame(gameModel) ?: false
|
||||||
|
if(success) {
|
||||||
|
launchOnUiThread {
|
||||||
|
viewModel.mainViewModel?.activity?.setFullScreen()
|
||||||
|
viewModel.mainViewModel?.navController?.navigate("game")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
|
@ -42,7 +42,7 @@ class MainView {
|
|||||||
@Composable
|
@Composable
|
||||||
fun Main(mainViewModel: MainViewModel) {
|
fun Main(mainViewModel: MainViewModel) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
mainViewModel.setNavController(navController)
|
mainViewModel.navController = navController
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "home") {
|
NavHost(navController = navController, startDestination = "home") {
|
||||||
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
|
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user