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(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 ce6b62ecae..f1413636e4 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, messages: List, 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, 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, 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. The hashes match The hashes do not match Close + Port + Forward port (UPnP)