Add ability to choose game when hosting

Also fix bottom sheets so they survive rotation
This commit is contained in:
Tom Pratt authored and Tom Pratt committed 2026-05-19 12:02:59 +02:00
1 parent 68e9fd6cde
commit 6655672ee3
9 files changed
+267 -14

No files matched your search

+3
View File
@@ -141,6 +141,7 @@ dependencies {
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)
// Kotlin extensions for lifecycle components // Kotlin extensions for lifecycle components
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -151,6 +152,7 @@ dependencies {
// For loading game covers from disk and GameTDB // For loading game covers from disk and GameTDB
implementation(libs.coil) implementation(libs.coil)
implementation(libs.coil.compose)
// For loading custom GPU drivers // For loading custom GPU drivers
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
@@ -164,6 +166,7 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material.icons) implementation(libs.androidx.compose.material.icons)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.tooling.preview)
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress 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.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
@@ -153,6 +154,8 @@ class NetplaySession(
fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer)
fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile)
fun startGame() = nativeStartGame() fun startGame() = nativeStartGame()
fun consumeBootSessionData(): Long { fun consumeBootSessionData(): Long {
@@ -232,6 +235,8 @@ class NetplaySession(
private external fun nativeReleaseBootSessionData(pointer: Long) private external fun nativeReleaseBootSessionData(pointer: Long)
private external fun nativeChangeGame(gameFile: GameFile)
private external fun nativeStartGame() private external fun nativeStartGame()
// NetPlayUI callbacks // NetPlayUI callbacks
@@ -4,17 +4,22 @@ package org.dolphinemu.dolphinemu.features.netplay.model
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig 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( class NetplayViewModel(
private val netplaySession: NetplaySession, private val netplaySession: NetplaySession,
@@ -41,10 +46,24 @@ class NetplayViewModel(
private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int)
val maxBuffer = _maxBuffer.asStateFlow() 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 saveTransferProgress = netplaySession.saveTransferProgress
val gameDigestProgress = netplaySession.gameDigestProgress val gameDigestProgress = netplaySession.gameDigestProgress
init {
if (netplaySession.isHosting) {
setInitialGame()
}
}
fun startGame() { fun startGame() {
netplaySession.startGame() netplaySession.startGame()
} }
@@ -64,6 +83,21 @@ class NetplayViewModel(
netplaySession.adjustPadBufferSize(buffer) 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) @OptIn(DelicateCoroutinesApi::class)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
@@ -9,6 +9,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
@@ -53,6 +54,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
game = viewModel.game.collectAsState().value, game = viewModel.game.collectAsState().value,
isHosting = viewModel.isHosting, isHosting = viewModel.isHosting,
onStartGame = viewModel::startGame, onStartGame = viewModel::startGame,
onGameSelected = viewModel::changeGame,
gameFiles = viewModel.gameFiles.collectAsState().value,
players = viewModel.players.collectAsState().value, players = viewModel.players.collectAsState().value,
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
maxBuffer = viewModel.maxBuffer.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value,
@@ -3,12 +3,14 @@
package org.dolphinemu.dolphinemu.features.netplay.ui package org.dolphinemu.dolphinemu.features.netplay.ui
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize 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.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions 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.material.icons.filled.Remove
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -42,9 +48,10 @@ import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -55,6 +62,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext 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.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.dolphinemu.dolphinemu.R 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.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress 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.DolphinTheme
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
import org.dolphinemu.dolphinemu.utils.CoilUtils
import java.util.Locale import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -91,6 +105,8 @@ fun NetplayScreen(
onSendMessage: (String) -> Unit, onSendMessage: (String) -> Unit,
game: String, game: String,
onStartGame: () -> Unit, onStartGame: () -> Unit,
onGameSelected: (GameFile) -> Unit,
gameFiles: List<GameFile>,
hostInputAuthorityEnabled: Boolean, hostInputAuthorityEnabled: Boolean,
maxBuffer: Int, maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit, onMaxBufferChanged: (Int) -> Unit,
@@ -125,12 +141,21 @@ fun NetplayScreen(
.consumeWindowInsets(innerPadding) .consumeWindowInsets(innerPadding)
.padding(innerPadding) .padding(innerPadding)
var showChat by rememberSaveable { mutableStateOf(false) }
var showGamePicker by rememberSaveable { mutableStateOf(false) }
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
LandscapeContent( LandscapeContent(
isHosting = isHosting, isHosting = isHosting,
messages = messages, messages = messages,
onSendMessage = onSendMessage, onSendMessage = onSendMessage,
showChat = showChat,
onShowChatChanged = { showChat = it },
game = game, game = game,
gameFiles = gameFiles,
onGameSelected = onGameSelected,
showGamePicker = showGamePicker,
onShowGamePickerChanged = { showGamePicker = it },
players = players, players = players,
hostInputAuthorityEnabled = hostInputAuthorityEnabled, hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer, maxBuffer = maxBuffer,
@@ -142,7 +167,13 @@ fun NetplayScreen(
isHosting = isHosting, isHosting = isHosting,
messages = messages, messages = messages,
onSendMessage = onSendMessage, onSendMessage = onSendMessage,
showChat = showChat,
onShowChatChanged = { showChat = it },
game = game, game = game,
gameFiles = gameFiles,
onGameSelected = onGameSelected,
showGamePicker = showGamePicker,
onShowGamePickerChanged = { showGamePicker = it },
players = players, players = players,
hostInputAuthorityEnabled = hostInputAuthorityEnabled, hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer, maxBuffer = maxBuffer,
@@ -201,7 +232,13 @@ private fun PortraitContent(
isHosting: Boolean, isHosting: Boolean,
messages: List<NetplayMessage>, messages: List<NetplayMessage>,
onSendMessage: (String) -> Unit, onSendMessage: (String) -> Unit,
showChat: Boolean,
onShowChatChanged: (Boolean) -> Unit,
game: String, game: String,
gameFiles: List<GameFile>,
onGameSelected: (GameFile) -> Unit,
showGamePicker: Boolean,
onShowGamePickerChanged: (Boolean) -> Unit,
players: List<Player>, players: List<Player>,
hostInputAuthorityEnabled: Boolean, hostInputAuthorityEnabled: Boolean,
maxBuffer: Int, maxBuffer: Int,
@@ -215,6 +252,8 @@ private fun PortraitContent(
Chat( Chat(
messages = messages, messages = messages,
onSendMessage = onSendMessage, onSendMessage = onSendMessage,
showBottomSheet = showChat,
onShowBottomSheetChanged = onShowChatChanged,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(200.dp) .height(200.dp)
@@ -225,10 +264,15 @@ private fun PortraitContent(
PlayersAndSettings( PlayersAndSettings(
game = game, game = game,
gameFiles = gameFiles,
onGameSelected = onGameSelected,
showGamePicker = showGamePicker,
onShowGamePickerChanged = onShowGamePickerChanged,
players = players, players = players,
hostInputAuthorityEnabled = hostInputAuthorityEnabled, hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer, maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged, onMaxBufferChanged = onMaxBufferChanged,
isHosting = isHosting,
modifier = Modifier modifier = Modifier
.padding(horizontal = DolphinTheme.scaffoldPadding), .padding(horizontal = DolphinTheme.scaffoldPadding),
) )
@@ -244,7 +288,13 @@ private fun LandscapeContent(
isHosting: Boolean, isHosting: Boolean,
messages: List<NetplayMessage>, messages: List<NetplayMessage>,
onSendMessage: (String) -> Unit, onSendMessage: (String) -> Unit,
showChat: Boolean,
onShowChatChanged: (Boolean) -> Unit,
game: String, game: String,
gameFiles: List<GameFile>,
onGameSelected: (GameFile) -> Unit,
showGamePicker: Boolean,
onShowGamePickerChanged: (Boolean) -> Unit,
players: List<Player>, players: List<Player>,
hostInputAuthorityEnabled: Boolean, hostInputAuthorityEnabled: Boolean,
maxBuffer: Int, maxBuffer: Int,
@@ -257,6 +307,8 @@ private fun LandscapeContent(
Chat( Chat(
messages = messages, messages = messages,
onSendMessage = onSendMessage, onSendMessage = onSendMessage,
showBottomSheet = showChat,
onShowBottomSheetChanged = onShowChatChanged,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxHeight() .fillMaxHeight()
@@ -270,10 +322,15 @@ private fun LandscapeContent(
) { ) {
PlayersAndSettings( PlayersAndSettings(
game = game, game = game,
gameFiles = gameFiles,
onGameSelected = onGameSelected,
showGamePicker = showGamePicker,
onShowGamePickerChanged = onShowGamePickerChanged,
players = players, players = players,
hostInputAuthorityEnabled = hostInputAuthorityEnabled, hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer, maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged, onMaxBufferChanged = onMaxBufferChanged,
isHosting = isHosting,
modifier = Modifier modifier = Modifier
.padding(horizontal = DolphinTheme.scaffoldPadding) .padding(horizontal = DolphinTheme.scaffoldPadding)
) )
@@ -288,22 +345,27 @@ private fun LandscapeContent(
@Composable @Composable
private fun PlayersAndSettings( private fun PlayersAndSettings(
game: String, game: String,
gameFiles: List<GameFile>,
onGameSelected: (GameFile) -> Unit,
showGamePicker: Boolean,
onShowGamePickerChanged: (Boolean) -> Unit,
players: List<Player>, players: List<Player>,
hostInputAuthorityEnabled: Boolean, hostInputAuthorityEnabled: Boolean,
maxBuffer: Int, maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit, onMaxBufferChanged: (Int) -> Unit,
isHosting: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier modifier = modifier
) { ) {
OutlinedTextField( GamePicker(
value = game, game = game,
onValueChange = {}, gameFiles = gameFiles,
label = { Text(stringResource(R.string.netplay_game_label)) }, onGameSelected = onGameSelected,
readOnly = true, showGamePicker = showGamePicker,
modifier = Modifier onShowGamePickerChanged = onShowGamePickerChanged,
.fillMaxWidth() isHosting = isHosting,
) )
MenuSpacer() MenuSpacer()
@@ -345,6 +407,8 @@ private fun PlayersAndSettings(
private fun Chat( private fun Chat(
messages: List<NetplayMessage>, messages: List<NetplayMessage>,
onSendMessage: (String) -> Unit, onSendMessage: (String) -> Unit,
showBottomSheet: Boolean,
onShowBottomSheetChanged: (Boolean) -> Unit,
modifier: Modifier, modifier: Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -361,12 +425,18 @@ private fun Chat(
draftMessage = "" draftMessage = ""
} }
var showBottomSheet by remember { mutableStateOf(false) } val density = LocalDensity.current
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val bottomSheetState = remember {
SheetState(
skipPartiallyExpanded = true,
density = density,
initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden,
)
}
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showBottomSheet = false }, onDismissRequest = { onShowBottomSheetChanged(false) },
sheetState = bottomSheetState, sheetState = bottomSheetState,
modifier = Modifier modifier = Modifier
.statusBarsPadding() .statusBarsPadding()
@@ -407,7 +477,7 @@ private fun Chat(
} }
OutlinedBox( OutlinedBox(
onClick = { showBottomSheet = true }, onClick = { onShowBottomSheetChanged(true) },
label = { Text(stringResource(R.string.netplay_chat_label)) }, label = { Text(stringResource(R.string.netplay_chat_label)) },
modifier = modifier 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 * 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. * first column which takes up the remaining space left by the other columns.
@@ -787,6 +974,8 @@ private fun PreviewNetplayScreen() {
game = "Game name", game = "Game name",
isHosting = true, isHosting = true,
onStartGame = {}, onStartGame = {},
onGameSelected = {},
gameFiles = emptyList(),
hostInputAuthorityEnabled = true, hostInputAuthorityEnabled = true,
maxBuffer = 10, maxBuffer = 10,
onMaxBufferChanged = {}, onMaxBufferChanged = {},
@@ -115,7 +115,8 @@ enum class StringSetting(
"" ""
), ),
NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"), 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 override val isOverridden: Boolean
get() = NativeConfig.isOverridden(file, section, key) get() = NativeConfig.isOverridden(file, section, key)
@@ -66,7 +66,7 @@ class GameCoverKeyer : Keyer<GameFile> {
} }
object CoilUtils { object CoilUtils {
private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext()) val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext())
.components { .components {
add(GameCoverKeyer()) add(GameCoverKeyer())
add(GameCoverFetcher.Factory()) add(GameCoverFetcher.Factory())
+3
View File
@@ -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-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-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 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 = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 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-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" } 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-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-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" } 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-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "tvprovider" }
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } 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" } 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" } filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref = "filepicker" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+15
View File
@@ -148,6 +148,21 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv
return reinterpret_cast<jlong>(server); 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 JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env,
jobject obj) jobject obj)