mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-05-09 04:13:28 -05:00
Add ability to choose game when hosting
Also fix bottom sheets so they survive rotation
This commit is contained in:
parent
309090520e
commit
39d17b2faf
|
|
@ -141,6 +141,7 @@ dependencies {
|
|||
implementation(libs.androidx.profileinstaller)
|
||||
|
||||
// Kotlin extensions for lifecycle components
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
|
|
@ -151,6 +152,7 @@ dependencies {
|
|||
|
||||
// For loading game covers from disk and GameTDB
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
// For loading custom GPU drivers
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
|
@ -164,6 +166,7 @@ dependencies {
|
|||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material.icons)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling.preview)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.runningFold
|
|||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||
import org.dolphinemu.dolphinemu.model.GameFile
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
||||
|
|
@ -149,6 +150,8 @@ class NetplaySession(
|
|||
|
||||
fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer)
|
||||
|
||||
fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile)
|
||||
|
||||
fun startGame() = nativeStartGame()
|
||||
|
||||
fun consumeBootSessionData(): Long {
|
||||
|
|
@ -228,6 +231,8 @@ class NetplaySession(
|
|||
|
||||
private external fun nativeReleaseBootSessionData(pointer: Long)
|
||||
|
||||
private external fun nativeChangeGame(gameFile: GameFile)
|
||||
|
||||
private external fun nativeStartGame()
|
||||
|
||||
// NetPlayUI callbacks
|
||||
|
|
|
|||
|
|
@ -4,17 +4,22 @@ package org.dolphinemu.dolphinemu.features.netplay.model
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
||||
import org.dolphinemu.dolphinemu.model.GameFile
|
||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||
|
||||
class NetplayViewModel(
|
||||
private val netplaySession: NetplaySession,
|
||||
|
|
@ -41,10 +46,24 @@ class NetplayViewModel(
|
|||
private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int)
|
||||
val maxBuffer = _maxBuffer.asStateFlow()
|
||||
|
||||
val gameFiles = GameFileCacheManager.getGameFiles().asFlow()
|
||||
.map { it.toList() }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(),
|
||||
GameFileCacheManager.getGameFiles().value?.toList() ?: emptyList()
|
||||
)
|
||||
|
||||
val saveTransferProgress = netplaySession.saveTransferProgress
|
||||
|
||||
val gameDigestProgress = netplaySession.gameDigestProgress
|
||||
|
||||
init {
|
||||
if (netplaySession.isHosting) {
|
||||
setInitialGame()
|
||||
}
|
||||
}
|
||||
|
||||
fun startGame() {
|
||||
netplaySession.startGame()
|
||||
}
|
||||
|
|
@ -64,6 +83,21 @@ class NetplayViewModel(
|
|||
netplaySession.adjustPadBufferSize(buffer)
|
||||
}
|
||||
|
||||
fun changeGame(gameFile: GameFile) {
|
||||
StringSetting.NETPLAY_GAME.setString(NativeConfig.LAYER_BASE, gameFile.getGameId())
|
||||
netplaySession.changeGame(gameFile)
|
||||
}
|
||||
|
||||
private fun setInitialGame() {
|
||||
val game = gameFiles.value
|
||||
.find { it.getGameId() == StringSetting.NETPLAY_GAME.string }
|
||||
?: gameFiles.value.firstOrNull()
|
||||
|
||||
if (game != null) {
|
||||
changeGame(game)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.activity.compose.setContent
|
|||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
|
|
@ -53,6 +54,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
|||
game = viewModel.game.collectAsState().value,
|
||||
isHosting = viewModel.isHosting,
|
||||
onStartGame = viewModel::startGame,
|
||||
onGameSelected = viewModel::changeGame,
|
||||
gameFiles = viewModel.gameFiles.collectAsState().value,
|
||||
players = viewModel.players.collectAsState().value,
|
||||
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
|
||||
maxBuffer = viewModel.maxBuffer.collectAsState().value,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
package org.dolphinemu.dolphinemu.features.netplay.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -19,6 +21,9 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -30,6 +35,7 @@ import androidx.compose.material.icons.filled.Add
|
|||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
|
@ -42,9 +48,10 @@ import androidx.compose.material3.MediumTopAppBar
|
|||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -55,6 +62,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -66,8 +75,11 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
|
|
@ -75,10 +87,12 @@ import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
|||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
||||
import org.dolphinemu.dolphinemu.model.GameFile
|
||||
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
||||
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
|
||||
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
|
||||
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
|
||||
import org.dolphinemu.dolphinemu.utils.CoilUtils
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -91,6 +105,8 @@ fun NetplayScreen(
|
|||
onSendMessage: (String) -> Unit,
|
||||
game: String,
|
||||
onStartGame: () -> Unit,
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
gameFiles: List<GameFile>,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
maxBuffer: Int,
|
||||
onMaxBufferChanged: (Int) -> Unit,
|
||||
|
|
@ -125,12 +141,21 @@ fun NetplayScreen(
|
|||
.consumeWindowInsets(innerPadding)
|
||||
.padding(innerPadding)
|
||||
|
||||
var showChat by rememberSaveable { mutableStateOf(false) }
|
||||
var showGamePicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
LandscapeContent(
|
||||
isHosting = isHosting,
|
||||
messages = messages,
|
||||
onSendMessage = onSendMessage,
|
||||
showChat = showChat,
|
||||
onShowChatChanged = { showChat = it },
|
||||
game = game,
|
||||
gameFiles = gameFiles,
|
||||
onGameSelected = onGameSelected,
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = { showGamePicker = it },
|
||||
players = players,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
maxBuffer = maxBuffer,
|
||||
|
|
@ -142,7 +167,13 @@ fun NetplayScreen(
|
|||
isHosting = isHosting,
|
||||
messages = messages,
|
||||
onSendMessage = onSendMessage,
|
||||
showChat = showChat,
|
||||
onShowChatChanged = { showChat = it },
|
||||
game = game,
|
||||
gameFiles = gameFiles,
|
||||
onGameSelected = onGameSelected,
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = { showGamePicker = it },
|
||||
players = players,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
maxBuffer = maxBuffer,
|
||||
|
|
@ -201,7 +232,13 @@ private fun PortraitContent(
|
|||
isHosting: Boolean,
|
||||
messages: List<NetplayMessage>,
|
||||
onSendMessage: (String) -> Unit,
|
||||
showChat: Boolean,
|
||||
onShowChatChanged: (Boolean) -> Unit,
|
||||
game: String,
|
||||
gameFiles: List<GameFile>,
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
maxBuffer: Int,
|
||||
|
|
@ -215,6 +252,8 @@ private fun PortraitContent(
|
|||
Chat(
|
||||
messages = messages,
|
||||
onSendMessage = onSendMessage,
|
||||
showBottomSheet = showChat,
|
||||
onShowBottomSheetChanged = onShowChatChanged,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
|
|
@ -225,10 +264,15 @@ private fun PortraitContent(
|
|||
|
||||
PLayersAndSettings(
|
||||
game = game,
|
||||
gameFiles = gameFiles,
|
||||
onGameSelected = onGameSelected,
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
players = players,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
maxBuffer = maxBuffer,
|
||||
onMaxBufferChanged = onMaxBufferChanged,
|
||||
isHosting = isHosting,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = DolphinTheme.scaffoldPadding),
|
||||
)
|
||||
|
|
@ -244,7 +288,13 @@ private fun LandscapeContent(
|
|||
isHosting: Boolean,
|
||||
messages: List<NetplayMessage>,
|
||||
onSendMessage: (String) -> Unit,
|
||||
showChat: Boolean,
|
||||
onShowChatChanged: (Boolean) -> Unit,
|
||||
game: String,
|
||||
gameFiles: List<GameFile>,
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
maxBuffer: Int,
|
||||
|
|
@ -257,6 +307,8 @@ private fun LandscapeContent(
|
|||
Chat(
|
||||
messages = messages,
|
||||
onSendMessage = onSendMessage,
|
||||
showBottomSheet = showChat,
|
||||
onShowBottomSheetChanged = onShowChatChanged,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
|
|
@ -270,10 +322,15 @@ private fun LandscapeContent(
|
|||
) {
|
||||
PLayersAndSettings(
|
||||
game = game,
|
||||
gameFiles = gameFiles,
|
||||
onGameSelected = onGameSelected,
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
players = players,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
maxBuffer = maxBuffer,
|
||||
onMaxBufferChanged = onMaxBufferChanged,
|
||||
isHosting = isHosting,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = DolphinTheme.scaffoldPadding)
|
||||
)
|
||||
|
|
@ -288,22 +345,27 @@ private fun LandscapeContent(
|
|||
@Composable
|
||||
private fun PLayersAndSettings(
|
||||
game: String,
|
||||
gameFiles: List<GameFile>,
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
maxBuffer: Int,
|
||||
onMaxBufferChanged: (Int) -> Unit,
|
||||
isHosting: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = game,
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.netplay_game_label)) },
|
||||
readOnly = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
GamePicker(
|
||||
game = game,
|
||||
gameFiles = gameFiles,
|
||||
onGameSelected = onGameSelected,
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
isHosting = isHosting,
|
||||
)
|
||||
|
||||
MenuSpacer()
|
||||
|
|
@ -345,6 +407,8 @@ private fun PLayersAndSettings(
|
|||
private fun Chat(
|
||||
messages: List<NetplayMessage>,
|
||||
onSendMessage: (String) -> Unit,
|
||||
showBottomSheet: Boolean,
|
||||
onShowBottomSheetChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -361,12 +425,18 @@ private fun Chat(
|
|||
draftMessage = ""
|
||||
}
|
||||
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val density = LocalDensity.current
|
||||
val bottomSheetState = remember {
|
||||
SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
density = density,
|
||||
initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden,
|
||||
)
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomSheet = false },
|
||||
onDismissRequest = { onShowBottomSheetChanged(false) },
|
||||
sheetState = bottomSheetState,
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
|
|
@ -407,7 +477,7 @@ private fun Chat(
|
|||
}
|
||||
|
||||
OutlinedBox(
|
||||
onClick = { showBottomSheet = true },
|
||||
onClick = { onShowBottomSheetChanged(true) },
|
||||
label = { Text(stringResource(R.string.netplay_chat_label)) },
|
||||
modifier = modifier
|
||||
) {
|
||||
|
|
@ -422,6 +492,123 @@ private fun Chat(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun GamePicker(
|
||||
game: String,
|
||||
gameFiles: List<GameFile>,
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
isHosting: Boolean,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val bottomSheetState = remember {
|
||||
SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
density = density,
|
||||
initialValue = if (showGamePicker) SheetValue.Expanded else SheetValue.Hidden,
|
||||
)
|
||||
}
|
||||
|
||||
if (showGamePicker) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onShowGamePickerChanged(false) },
|
||||
sheetState = bottomSheetState,
|
||||
modifier = Modifier.statusBarsPadding()
|
||||
) {
|
||||
GameList(
|
||||
gameFiles = gameFiles,
|
||||
onGameSelected = { gameFile ->
|
||||
onGameSelected(gameFile)
|
||||
onShowGamePickerChanged(false)
|
||||
},
|
||||
contentPadding = PaddingValues(
|
||||
start = DolphinTheme.scaffoldPadding,
|
||||
end = DolphinTheme.scaffoldPadding,
|
||||
bottom = 16.dp
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = game,
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.netplay_game_label)) },
|
||||
readOnly = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (isHosting) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(top = 8.dp)
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.clickable { onShowGamePickerChanged(true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GameList(
|
||||
gameFiles: List<GameFile>,
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
contentPadding: PaddingValues = PaddingValues(),
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||
contentPadding = contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(gameFiles, key = { it.getPath() }) { gameFile ->
|
||||
GameGridItem(
|
||||
gameFile = gameFile,
|
||||
onClick = { onGameSelected(gameFile) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GameGridItem(
|
||||
gameFile: GameFile,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
) {
|
||||
Column {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(gameFile)
|
||||
.error(R.drawable.no_banner)
|
||||
.build(),
|
||||
contentDescription = gameFile.getTitle(),
|
||||
contentScale = ContentScale.Crop,
|
||||
imageLoader = CoilUtils.imageLoader,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.7f)
|
||||
)
|
||||
Text(
|
||||
text = gameFile.getTitle(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2,
|
||||
minLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A table arranged into columns sized to wrap the largest item. Except the
|
||||
* first column which takes up the remaining space left by the other columns.
|
||||
|
|
@ -787,6 +974,8 @@ private fun PreviewNetplayScreen() {
|
|||
game = "Game name",
|
||||
isHosting = true,
|
||||
onStartGame = {},
|
||||
onGameSelected = {},
|
||||
gameFiles = emptyList(),
|
||||
hostInputAuthorityEnabled = true,
|
||||
maxBuffer = 10,
|
||||
onMaxBufferChanged = {},
|
||||
|
|
|
|||
|
|
@ -115,7 +115,8 @@ enum class StringSetting(
|
|||
""
|
||||
),
|
||||
NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"),
|
||||
NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player");
|
||||
NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"),
|
||||
NETPLAY_GAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Game", "");
|
||||
|
||||
override val isOverridden: Boolean
|
||||
get() = NativeConfig.isOverridden(file, section, key)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class GameCoverKeyer : Keyer<GameFile> {
|
|||
}
|
||||
|
||||
object CoilUtils {
|
||||
private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext())
|
||||
val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext())
|
||||
.components {
|
||||
add(GameCoverKeyer())
|
||||
add(GameCoverFetcher.Factory())
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ androidx-cardview = { group = "androidx.cardview", name = "cardview", version.re
|
|||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
|
|
@ -45,6 +46,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
|
|||
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
|
||||
androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" }
|
||||
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
|
||||
|
|
@ -55,6 +57,7 @@ androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "s
|
|||
androidx-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "tvprovider" }
|
||||
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
desugar_jdk_libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref = "filepicker" }
|
||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||
|
|
|
|||
|
|
@ -148,6 +148,21 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv
|
|||
return reinterpret_cast<jlong>(server);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeChangeGame(JNIEnv* env,
|
||||
jobject obj,
|
||||
jobject jgame_file)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
const auto& game_file = *reinterpret_cast<std::shared_ptr<const UICommon::GameFile>*>(
|
||||
env->GetLongField(jgame_file, IDCache::GetGameFilePointer()));
|
||||
|
||||
server->ChangeGame(game_file->GetSyncIdentifier(), game_file->GetLongName());
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env,
|
||||
jobject obj)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user