Hosting UI

This commit is contained in:
Tom Pratt 2026-05-06 10:43:26 +02:00
parent 7ea7e638bd
commit ccce2b2e9a
9 changed files with 163 additions and 15 deletions

View File

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.features.netplay.NetplayManager
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
@ -48,6 +49,12 @@ class NetplaySetupViewModel(
private val _connectPort = MutableStateFlow(IntSetting.NETPLAY_CONNECT_PORT.int.toString())
val connectPort = _connectPort.asStateFlow()
private val _hostPort = MutableStateFlow(IntSetting.NETPLAY_HOST_PORT.int.toString())
val hostPort = _hostPort.asStateFlow()
private val _useUpnp = MutableStateFlow(BooleanSetting.NETPLAY_USE_UPNP.boolean)
val useUpnp = _useUpnp.asStateFlow()
private val _showNetplayScreen = Channel<Unit>(CONFLATED)
val showNetplayScreen = _showNetplayScreen.receiveAsFlow()
@ -98,6 +105,24 @@ class NetplaySetupViewModel(
}
}
fun setHostPort(port: String) {
if (port.all { it.isDigit() }) {
_hostPort.value = port
port.toIntOrNull()?.let {
IntSetting.NETPLAY_HOST_PORT.setInt(NativeConfig.LAYER_BASE, it)
}
}
}
fun setUseUpnp(useUpnp: Boolean) {
_useUpnp.value = useUpnp
BooleanSetting.NETPLAY_USE_UPNP.setBoolean(NativeConfig.LAYER_BASE, useUpnp)
}
fun host() {
}
fun connect() {
if (_connecting.value) return

View File

@ -51,6 +51,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
messages = viewModel.messages.collectAsState().value,
onSendMessage = viewModel::sendMessage,
game = viewModel.game.collectAsState().value,
isHosting = false,
onStartGame = {},
players = viewModel.players.collectAsState().value,
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
maxBuffer = viewModel.maxBuffer.collectAsState().value,

View File

@ -31,6 +31,7 @@ import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -84,10 +85,12 @@ import java.util.Locale
@Composable
fun NetplayScreen(
onBackClicked: () -> Unit,
isHosting: Boolean,
connectionLost: Flow<Unit>,
messages: List<NetplayMessage>,
onSendMessage: (String) -> Unit,
game: String,
onStartGame: () -> Unit,
hostInputAuthorityEnabled: Boolean,
maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit,
@ -109,6 +112,13 @@ fun NetplayScreen(
},
)
},
floatingActionButton = {
if (isHosting) {
ExtendedFloatingActionButton(onClick = onStartGame) {
Text(stringResource(R.string.netplay_start))
}
}
},
) { innerPadding ->
val modifier = Modifier
.fillMaxSize()
@ -117,6 +127,7 @@ fun NetplayScreen(
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
LandscapeContent(
isHosting = isHosting,
messages = messages,
onSendMessage = onSendMessage,
game = game,
@ -128,6 +139,7 @@ fun NetplayScreen(
)
} else {
PortraitContent(
isHosting = isHosting,
messages = messages,
onSendMessage = onSendMessage,
game = game,
@ -186,6 +198,7 @@ fun NetplayScreen(
@Composable
private fun PortraitContent(
isHosting: Boolean,
messages: List<NetplayMessage>,
onSendMessage: (String) -> Unit,
game: String,
@ -197,13 +210,14 @@ private fun PortraitContent(
) {
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
) {
Chat(
messages = messages,
onSendMessage = onSendMessage,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.3f)
.height(200.dp)
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
@ -216,14 +230,18 @@ private fun PortraitContent(
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
modifier = Modifier
.weight(1f)
.padding(horizontal = DolphinTheme.scaffoldPadding),
)
if (isHosting) {
Spacer(modifier = Modifier.height(DolphinTheme.fabClearancePadding))
}
}
}
@Composable
private fun LandscapeContent(
isHosting: Boolean,
messages: List<NetplayMessage>,
onSendMessage: (String) -> Unit,
game: String,
@ -245,16 +263,25 @@ private fun LandscapeContent(
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
PLayersAndSettings(
game = game,
players = players,
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
.verticalScroll(rememberScrollState())
) {
PLayersAndSettings(
game = game,
players = players,
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
modifier = Modifier
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
if (isHosting) {
Spacer(modifier = Modifier.height(DolphinTheme.fabClearancePadding))
}
}
}
}
@ -269,7 +296,6 @@ private fun PLayersAndSettings(
) {
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
) {
OutlinedTextField(
value = game,
@ -759,6 +785,8 @@ private fun PreviewNetplayScreen() {
},
onSendMessage = {},
game = "Game name",
isHosting = true,
onStartGame = {},
hostInputAuthorityEnabled = true,
maxBuffer = 10,
onMaxBufferChanged = {},

View File

@ -57,6 +57,11 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
onConnectPortChanged = viewModel::setConnectPort,
hostCode = viewModel.hostCode.collectAsState().value,
onHostCodeChanged = viewModel::setHostCode,
hostPort = viewModel.hostPort.collectAsState().value,
onHostPortChanged = viewModel::setHostPort,
useUpnp = viewModel.useUpnp.collectAsState().value,
onUseUpnpChanged = viewModel::setUseUpnp,
onHostClicked = viewModel::host,
onConnectClicked = viewModel::connect,
)
}

View File

@ -3,10 +3,12 @@
package org.dolphinemu.dolphinemu.features.netplay.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -16,6 +18,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@ -27,6 +30,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SecondaryTabRow
@ -39,6 +43,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
@ -74,6 +79,11 @@ fun NetplaySetupScreen(
onConnectPortChanged: (String) -> Unit,
hostCode: String,
onHostCodeChanged: (String) -> Unit,
hostPort: String,
onHostPortChanged: (String) -> Unit,
useUpnp: Boolean,
onUseUpnpChanged: (Boolean) -> Unit,
onHostClicked: () -> Unit,
onConnectClicked: () -> Unit,
) {
Scaffold(
@ -92,7 +102,10 @@ fun NetplaySetupScreen(
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = onConnectClicked,
onClick = when(connectionRole) {
ConnectionRole.Host -> onHostClicked
ConnectionRole.Connect -> onConnectClicked
},
) {
if (connecting) {
CircularProgressIndicator(
@ -134,6 +147,7 @@ fun NetplaySetupScreen(
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
.padding(innerPadding)
.padding(bottom = DolphinTheme.fabClearancePadding)
) {
SecondaryTabRow(selectedTabIndex = ConnectionRole.all.indexOf(connectionRole)) {
ConnectionRole.all.forEach { role ->
@ -162,6 +176,10 @@ fun NetplaySetupScreen(
onHostCodeChanged = onHostCodeChanged,
connectPort = connectPort,
onConnectPortChanged = onConnectPortChanged,
hostPort = hostPort,
onHostPortChanged = onHostPortChanged,
useUpnp = useUpnp,
onUseUpnpChanged = onUseUpnpChanged,
connectionRole = connectionRole,
)
}
@ -181,6 +199,10 @@ private fun NetplaySetupContent(
onHostCodeChanged: (String) -> Unit,
connectPort: String,
onConnectPortChanged: (String) -> Unit,
hostPort: String,
onHostPortChanged: (String) -> Unit,
useUpnp: Boolean,
onUseUpnpChanged: (Boolean) -> Unit,
connectionRole: ConnectionRole,
) {
OutlinedTextField(
@ -211,7 +233,13 @@ private fun NetplaySetupContent(
onPortChanged = onConnectPortChanged,
)
ConnectionRole.Host -> {}
ConnectionRole.Host -> HostMenu(
connectionType = connectionType,
hostPort = hostPort,
onHostPortChanged = onHostPortChanged,
useUpnp = useUpnp,
onUseUpnpChanged = onUseUpnpChanged,
)
}
}
@ -304,6 +332,56 @@ fun ConnectMenu(
}
}
@Composable
private fun HostMenu(
connectionType: ConnectionType,
hostPort: String,
onHostPortChanged: (String) -> Unit,
useUpnp: Boolean,
onUseUpnpChanged: (Boolean) -> Unit,
) {
if (connectionType == ConnectionType.DirectConnection) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
OutlinedTextField(
value = hostPort,
onValueChange = onHostPortChanged,
label = { Text(stringResource(R.string.netplay_host_port_label)) },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
singleLine = true,
modifier = Modifier
.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
OutlinedButton(
onClick = { onUseUpnpChanged(!useUpnp) },
shape = MaterialTheme.shapes.extraSmall,
modifier = Modifier
.height(64.dp)
.padding(top = 8.dp)
) {
Text(
text = stringResource(R.string.netplay_use_upnp),
)
Spacer(modifier = Modifier.width(12.dp))
Checkbox(
checked = useUpnp,
onCheckedChange = null,
modifier = Modifier.size(24.dp),
)
}
}
MenuSpacer()
}
}
@Preview
@Composable
private fun NetplaySetupScreenPreview() {
@ -312,7 +390,7 @@ private fun NetplaySetupScreenPreview() {
onBackClicked = {},
connecting = false,
errors = emptyFlow(),
connectionRole = ConnectionRole.Connect,
connectionRole = ConnectionRole.Host,
onConnectionRoleChanged = {},
nickname = "Preview nickname",
onNicknameChanged = {},
@ -324,6 +402,11 @@ private fun NetplaySetupScreenPreview() {
onConnectPortChanged = {},
hostCode = "",
onHostCodeChanged = {},
hostPort = "2626",
onHostPortChanged = {},
useUpnp = false,
onUseUpnpChanged = {},
onHostClicked = {},
onConnectClicked = {},
)
}

View File

@ -933,7 +933,8 @@ enum class BooleanSetting(
Settings.SECTION_ACHIEVEMENTS,
"ProgressEnabled",
false
);
),
NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false);
override val isOverridden: Boolean
get() = NativeConfig.isOverridden(file, section, key)

View File

@ -140,6 +140,7 @@ enum class IntSetting(
WIIMOTE_4_SOURCE(Settings.FILE_WIIMOTE, "Wiimote4", "Source", 0),
WIIMOTE_BB_SOURCE(Settings.FILE_WIIMOTE, "BalanceBoard", "Source", 0),
NETPLAY_CONNECT_PORT(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "ConnectPort", 2626),
NETPLAY_HOST_PORT(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "HostPort", 2626),
NETPLAY_CLIENT_BUFFER_SIZE(
Settings.FILE_DOLPHIN,
Settings.SECTION_INI_NETPLAY,

View File

@ -36,6 +36,7 @@ import com.google.android.material.R as MaterialR
object DolphinTheme {
val scaffoldPadding = 16.dp
val fabClearancePadding = 80.dp
}
@Composable

View File

@ -1014,4 +1014,6 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_game_digest_match">The hashes match</string>
<string name="netplay_game_digest_mismatch">The hashes do not match</string>
<string name="netplay_game_digest_close">Close</string>
<string name="netplay_host_port_label">Port</string>
<string name="netplay_use_upnp">Forward port (UPnP)</string>
</resources>