summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java/org
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-04-15 17:33:09 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:53 +0200
commitf10022f2dc6e5e7d9636d3e91fe1dc3737dc703e (patch)
tree9563a2f810951f7f8b3ca77486f28db2be48509e /Source/Android/app/src/main/java/org
parentc716c32ce891b340f5b287f6ae589e43832c427e (diff)
Netplay setup UI
Only for connecting, no hosting yet.
Diffstat (limited to 'Source/Android/app/src/main/java/org')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt16
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt21
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt58
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt240
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt6
5 files changed, 338 insertions, 3 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt
new file mode 100644
index 0000000000..a54d082566
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionRole.kt
@@ -0,0 +1,16 @@
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+import androidx.annotation.StringRes
+import org.dolphinemu.dolphinemu.R
+
+sealed class ConnectionRole(
+ @StringRes val labelId: Int,
+) {
+ object Connect : ConnectionRole(R.string.netplay_connection_role_connect)
+ object Host : ConnectionRole(R.string.netplay_connection_role_host)
+
+ companion object {
+ val all: List<ConnectionRole>
+ get() = listOf(Connect, Host)
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt
new file mode 100644
index 0000000000..aed03f89cc
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ConnectionType.kt
@@ -0,0 +1,21 @@
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+import androidx.annotation.StringRes
+import org.dolphinemu.dolphinemu.R
+
+sealed class ConnectionType(
+ @StringRes val labelId: Int,
+) {
+ object DirectConnection : ConnectionType(
+ labelId = R.string.netplay_connection_type_direct_connection,
+ )
+
+ object TraversalServer : ConnectionType(
+ labelId = R.string.netplay_connection_type_traversal_server,
+ )
+
+ companion object {
+ val all: List<ConnectionType>
+ get() = listOf(DirectConnection, TraversalServer)
+ }
+}
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
new file mode 100644
index 0000000000..8d0dd05c79
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class NetplaySetupViewModel : ViewModel() {
+ private val _connectionRole = MutableStateFlow<ConnectionRole>(ConnectionRole.Connect)
+ val connectionRole = _connectionRole.asStateFlow()
+
+ private val _nickname = MutableStateFlow("")
+ val nickname = _nickname.asStateFlow()
+
+ private val _connectionType = MutableStateFlow<ConnectionType>(ConnectionType.DirectConnection)
+ val connectionType = _connectionType.asStateFlow()
+
+ private val _ipAddress = MutableStateFlow("")
+ val ipAddress = _ipAddress.asStateFlow()
+
+ private val _hostCode = MutableStateFlow("")
+ val hostCode = _hostCode.asStateFlow()
+
+ private val _connectPort = MutableStateFlow(0.toString())
+ val connectPort = _connectPort.asStateFlow()
+
+ fun setConnectionRole(connectionRole: ConnectionRole) {
+ _connectionRole.value = connectionRole
+ }
+
+ fun setNickname(nickname: String) {
+ _nickname.value = nickname
+ }
+
+ fun setConnectionType(connectionType: ConnectionType) {
+ _connectionType.value = connectionType
+ }
+
+ fun setIpAddress(ipAddress: String) {
+ if (ipAddress.all { it.isDigit() || it == '.' }) {
+ _ipAddress.value = ipAddress
+ }
+ }
+
+ fun setHostCode(hostCode: String) {
+ _hostCode.value = hostCode
+ }
+
+ fun setConnectPort(port: String) {
+ if (port.all { it.isDigit() }) {
+ _connectPort.value = port
+ }
+ }
+
+ fun connect() {
+ }
+}
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 f79584c484..71d83ddf81 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
@@ -12,39 +12,77 @@ import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
+import androidx.compose.material3.MenuAnchorType
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SecondaryTabRow
+import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.ViewModelProvider
import org.dolphinemu.dolphinemu.R
+import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionRole
+import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType
+import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
+import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
import org.dolphinemu.dolphinemu.utils.ThemeHelper
class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
override var themeId: Int = 0
+ private lateinit var viewModel: NetplaySetupViewModel
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
enableEdgeToEdge()
super.onCreate(savedInstanceState)
+ viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java]
+
setContent {
DolphinTheme {
NetplaySetupScreen(
onBackClicked = { finish() },
+ nickname = viewModel.nickname.collectAsState().value,
+ onNicknameChanged = viewModel::setNickname,
+ connectionType = viewModel.connectionType.collectAsState().value,
+ onConnectionTypeChanged = viewModel::setConnectionType,
+ connectionRole = viewModel.connectionRole.collectAsState().value,
+ onConnectionRoleChanged = viewModel::setConnectionRole,
+ ipAddress = viewModel.ipAddress.collectAsState().value,
+ onIpAddressChanged = viewModel::setIpAddress,
+ connectPort = viewModel.connectPort.collectAsState().value,
+ onConnectPortChanged = viewModel::setConnectPort,
+ hostCode = viewModel.hostCode.collectAsState().value,
+ onHostCodeChanged = viewModel::setHostCode,
+ onConnectClicked = viewModel::connect,
)
}
}
@@ -71,6 +109,19 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
@Composable
private fun NetplaySetupScreen(
onBackClicked: () -> Unit,
+ connectionRole: ConnectionRole,
+ onConnectionRoleChanged: (ConnectionRole) -> Unit,
+ nickname: String,
+ onNicknameChanged: (String) -> Unit,
+ connectionType: ConnectionType,
+ onConnectionTypeChanged: (ConnectionType) -> Unit,
+ ipAddress: String,
+ onIpAddressChanged: (String) -> Unit,
+ connectPort: String,
+ onConnectPortChanged: (String) -> Unit,
+ hostCode: String,
+ onHostCodeChanged: (String) -> Unit,
+ onConnectClicked: () -> Unit,
) {
Scaffold(
topBar = {
@@ -85,16 +136,184 @@ private fun NetplaySetupScreen(
}
},
)
+ },
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ onClick = onConnectClicked,
+ ) {
+ Text(stringResource(connectionRole.labelId))
+ }
}
) { innerPadding ->
Column(
modifier = Modifier
+ .fillMaxSize()
.consumeWindowInsets(innerPadding)
+ .verticalScroll(rememberScrollState())
.padding(innerPadding)
- .padding(horizontal = DolphinTheme.scaffoldPadding)
- .verticalScroll(rememberScrollState()),
) {
+ SecondaryTabRow(selectedTabIndex = ConnectionRole.all.indexOf(connectionRole)) {
+ ConnectionRole.all.forEach { role ->
+ Tab(
+ selected = connectionRole == role,
+ onClick = { onConnectionRoleChanged(role) },
+ text = { Text(stringResource(role.labelId)) },
+ )
+ }
+ }
+
+ MenuSpacer()
+
+ Column(
+ modifier = Modifier
+ .padding(horizontal = DolphinTheme.scaffoldPadding)
+ ) {
+ NetplaySetupContent(
+ nickname = nickname,
+ onNicknameChanged = onNicknameChanged,
+ connectionType = connectionType,
+ onConnectionTypeChanged = onConnectionTypeChanged,
+ ipAddress = ipAddress,
+ onIpAddressChanged = onIpAddressChanged,
+ hostCode = hostCode,
+ onHostCodeChanged = onHostCodeChanged,
+ connectPort = connectPort,
+ onConnectPortChanged = onConnectPortChanged,
+ connectionRole = connectionRole,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun NetplaySetupContent(
+ nickname: String,
+ onNicknameChanged: (String) -> Unit,
+ connectionType: ConnectionType,
+ onConnectionTypeChanged: (ConnectionType) -> Unit,
+ ipAddress: String,
+ onIpAddressChanged: (String) -> Unit,
+ hostCode: String,
+ onHostCodeChanged: (String) -> Unit,
+ connectPort: String,
+ onConnectPortChanged: (String) -> Unit,
+ connectionRole: ConnectionRole,
+) {
+ OutlinedTextField(
+ value = nickname,
+ onValueChange = onNicknameChanged,
+ label = { Text(stringResource(R.string.netplay_nickname_label)) },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ MenuSpacer()
+
+ ConnectionTypePicker(
+ connectionType = connectionType,
+ onConnectionTypeChanged = onConnectionTypeChanged,
+ )
+
+ MenuSpacer()
+
+ when (connectionRole) {
+ ConnectionRole.Connect -> ConnectMenu(
+ connectionType = connectionType,
+ ipAddress = ipAddress,
+ onIpAddressChanged = onIpAddressChanged,
+ hostCode = hostCode,
+ onHostCodeChanged = onHostCodeChanged,
+ port = connectPort,
+ onPortChanged = onConnectPortChanged,
+ )
+
+ ConnectionRole.Host -> {}
+ }
+}
+
+@Composable
+private fun ConnectionTypePicker(
+ connectionType: ConnectionType,
+ onConnectionTypeChanged: (ConnectionType) -> Unit,
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ ConnectionType.all.forEach { connectionType ->
+ DropdownMenuItem(
+ text = { Text(stringResource(connectionType.labelId)) },
+ onClick = {
+ onConnectionTypeChanged(connectionType)
+ expanded = false
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ OutlinedTextField(
+ value = stringResource(connectionType.labelId),
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(stringResource(R.string.netplay_connection_type)) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+ modifier = Modifier
+ .menuAnchor(MenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth()
+ )
+ }
+}
+
+@Composable
+fun ConnectMenu(
+ connectionType: ConnectionType,
+ ipAddress: String,
+ onIpAddressChanged: (String) -> Unit,
+ hostCode: String,
+ onHostCodeChanged: (String) -> Unit,
+ port: String,
+ onPortChanged: (String) -> Unit,
+) {
+ when (connectionType) {
+ ConnectionType.DirectConnection -> {
+ OutlinedTextField(
+ value = ipAddress,
+ onValueChange = onIpAddressChanged,
+ label = { Text(stringResource(R.string.netplay_ip_address_label)) },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+
+ MenuSpacer()
+
+ OutlinedTextField(
+ value = port,
+ onValueChange = onPortChanged,
+ label = { Text(stringResource(R.string.netplay_port_label)) },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ )
}
+
+ ConnectionType.TraversalServer -> OutlinedTextField(
+ value = hostCode,
+ onValueChange = onHostCodeChanged,
+ label = { Text(stringResource(R.string.netplay_host_code_label)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ )
}
}
@@ -102,6 +321,21 @@ private fun NetplaySetupScreen(
@Composable
private fun NetplaySetupScreenPreview() {
MaterialTheme {
- NetplayScreen(onBackClicked = {})
+ NetplaySetupScreen(
+ onBackClicked = {},
+ nickname = "Preview nickname",
+ onNicknameChanged = {},
+ connectionType = ConnectionType.DirectConnection,
+ onConnectionTypeChanged = {},
+ connectionRole = ConnectionRole.Connect,
+ onConnectionRoleChanged = {},
+ ipAddress = "127.0.0.1",
+ onIpAddressChanged = {},
+ connectPort = "2626",
+ onConnectPortChanged = {},
+ hostCode = "",
+ onHostCodeChanged = {},
+ onConnectClicked = {},
+ )
}
}
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 58eb52b347..24b0f99c0a 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
@@ -5,12 +5,15 @@ package org.dolphinemu.dolphinemu.ui.theme
import android.content.Context
import androidx.annotation.AttrRes
import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@@ -101,3 +104,6 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
)
}
}
+
+@Composable
+fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp))