summaryrefslogtreecommitdiff
path: root/Source
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-05-06 10:43:26 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:59 +0200
commitb63ccacdfa7355989e0ca971b9b06ef1dcfb3bbb (patch)
treecade216acbf52d016d5eb4a02f349b087bbd3946 /Source
parentf9baecd5eda524a0227ed4697b6853fb1a446b18 (diff)
Hosting UI
Diffstat (limited to 'Source')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt25
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt50
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt5
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt89
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt1
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt1
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml2
9 files changed, 163 insertions, 15 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
index a9b34d3147..d51ae477ac 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
@@ -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
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
index d99c4da57a..c01ec2a76c 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
@@ -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,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
index 71595ddb43..45bd89efab 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
@@ -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 = {},
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt
index 145cf7d635..a82dbff15a 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt
@@ -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,
)
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt
index bcc11ef4bb..939a39524d 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt
@@ -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 = {},
)
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
index cd0ea42d94..4658814ec1 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
@@ -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)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt
index d8bc8fbba5..85f265442b 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/IntSetting.kt
@@ -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,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt
index cf768e6dc0..d25a345862 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt
@@ -36,6 +36,7 @@ import com.google.android.material.R as MaterialR
object DolphinTheme {
val scaffoldPadding = 16.dp
+ val fabClearancePadding = 80.dp
}
@Composable
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index ef103432aa..9c89d3aed7 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -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>