summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-05-03 23:23:07 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:03:00 +0200
commit86a0f228876a101dfb3eafb08a56138eaa4b7e68 (patch)
tree8297ea5c18276166d44b8ee475cc9fc3e648a96b /Source/Android/app/src/main
parenteb02680cb6ad8a8b700ccf37608c064fbb24fb8c (diff)
Show joining info for local and external IP addresses
Doesn't support traversal yet
Diffstat (limited to 'Source/Android/app/src/main')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt8
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt17
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt37
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt231
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt36
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt5
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml8
8 files changed, 320 insertions, 26 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
index 3ec1dea43f..8af9cf9806 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
@@ -158,6 +158,10 @@ class NetplaySession(
fun startGame() = nativeStartGame()
+ fun getPort(): Int = nativeGetPort()
+
+ fun getExternalIpAddress(): String? = nativeGetExternalIpAddress()
+
fun consumeBootSessionData(): Long {
return bootSessionDataPointer.also {
bootSessionDataPointer = 0
@@ -239,6 +243,10 @@ class NetplaySession(
private external fun nativeStartGame()
+ private external fun nativeGetPort(): Int
+
+ private external fun nativeGetExternalIpAddress(): String?
+
// NetPlayUI callbacks
@Keep
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt
new file mode 100644
index 0000000000..ba350a3caf
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/JoinInfo.kt
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+import androidx.annotation.StringRes
+import org.dolphinemu.dolphinemu.R
+
+enum class JoinInfoType(@StringRes val labelId: Int) {
+ EXTERNAL(R.string.netplay_address_type_external),
+ LOCAL(R.string.netplay_address_type_local),
+}
+
+sealed class JoinAddress {
+ data object Loading : JoinAddress()
+ data class Loaded(val address: String) : JoinAddress()
+ data class Unknown(val retry: () -> Unit) : JoinAddress()
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
index 50edaba597..923cfc314d 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
@@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -20,15 +21,25 @@ 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
+import org.dolphinemu.dolphinemu.utils.NetworkHelper
class NetplayViewModel(
private val netplaySession: NetplaySession,
+ private val networkHelper: NetworkHelper,
) : ViewModel() {
val launchGame = netplaySession.launchGame
val isHosting = netplaySession.isHosting
+ private val _joinAddresses = MutableStateFlow(
+ mapOf(
+ JoinInfoType.EXTERNAL to JoinAddress.Loading,
+ JoinInfoType.LOCAL to getLocalIp(),
+ )
+ )
+ val joinAddresses = _joinAddresses.asStateFlow()
+
val connectionLost = netplaySession.connectionLost
val players = netplaySession.players
@@ -61,6 +72,7 @@ class NetplayViewModel(
init {
if (netplaySession.isHosting) {
setInitialGame()
+ fetchExternalIp()
}
}
@@ -88,6 +100,24 @@ class NetplayViewModel(
netplaySession.changeGame(gameFile)
}
+ private fun getLocalIp(): JoinAddress {
+ val localIp = networkHelper.getLocalIpString()
+ ?: return JoinAddress.Unknown { _joinAddresses.value += JoinInfoType.LOCAL to getLocalIp() }
+ val port = netplaySession.getPort()
+ return JoinAddress.Loaded("$localIp:$port")
+ }
+
+ private fun fetchExternalIp() {
+ _joinAddresses.value += JoinInfoType.EXTERNAL to JoinAddress.Loading
+ viewModelScope.launch(Dispatchers.IO) {
+ val ip = netplaySession.getExternalIpAddress()
+ val port = netplaySession.getPort()
+ val address = if (ip != null) JoinAddress.Loaded("$ip:$port")
+ else JoinAddress.Unknown { fetchExternalIp() }
+ _joinAddresses.value += JoinInfoType.EXTERNAL to address
+ }
+ }
+
private fun setInitialGame() {
val game = gameFiles.value
.find { it.getGameId() == StringSetting.NETPLAY_GAME.string }
@@ -108,10 +138,13 @@ class NetplayViewModel(
}
}
- class Factory(private val session: NetplaySession) : ViewModelProvider.Factory {
+ class Factory(
+ private val session: NetplaySession,
+ private val networkHelper: NetworkHelper,
+ ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
- return NetplayViewModel(session) as T
+ return NetplayViewModel(session, networkHelper) as T
}
}
}
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 55888dde6d..a9bf0309a5 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
@@ -21,6 +21,7 @@ import org.dolphinemu.dolphinemu.features.netplay.NetplayManager
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
+import org.dolphinemu.dolphinemu.utils.NetworkHelper
import org.dolphinemu.dolphinemu.utils.ThemeHelper
class NetplayActivity : AppCompatActivity(), ThemeProvider {
@@ -37,7 +38,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
return
}
- val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session))[NetplayViewModel::class.java]
+ val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session, NetworkHelper))[NetplayViewModel::class.java]
viewModel.launchGame
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
@@ -62,6 +63,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
onMaxBufferChanged = viewModel::setMaxBuffer,
saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value,
gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value,
+ joinAddresses = viewModel.joinAddresses.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 86cbfe5129..eeba0f1541 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
@@ -2,10 +2,11 @@
package org.dolphinemu.dolphinemu.features.netplay.ui
+import android.content.Intent
import android.content.res.Configuration
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -17,6 +18,7 @@ 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.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
@@ -32,11 +34,17 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+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.HorizontalDivider
import androidx.compose.material3.Icon
@@ -45,6 +53,7 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
+import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
@@ -62,7 +71,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.platform.LocalConfiguration
@@ -84,6 +92,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
+import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress
+import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
@@ -92,6 +102,7 @@ import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
+import org.dolphinemu.dolphinemu.ui.theme.ReadOnlyTextField
import org.dolphinemu.dolphinemu.utils.CoilUtils
import java.util.Locale
@@ -113,6 +124,7 @@ fun NetplayScreen(
players: List<Player>,
saveTransferProgress: SaveTransferProgress?,
gameDigestProgress: GameDigestProgress?,
+ joinAddresses: Map<JoinInfoType, JoinAddress>,
) {
Scaffold(
topBar = {
@@ -141,8 +153,10 @@ fun NetplayScreen(
.consumeWindowInsets(innerPadding)
.padding(innerPadding)
+ // State which must live above the landscape/portrait split.
var showChat by rememberSaveable { mutableStateOf(false) }
var showGamePicker by rememberSaveable { mutableStateOf(false) }
+ var selectedJoinInfoType by rememberSaveable { mutableStateOf(JoinInfoType.EXTERNAL) }
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
LandscapeContent(
@@ -160,6 +174,9 @@ fun NetplayScreen(
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
+ joinAddresses = joinAddresses,
+ selectedJoinInfoType = selectedJoinInfoType,
+ onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it },
modifier = modifier
)
} else {
@@ -178,6 +195,9 @@ fun NetplayScreen(
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
+ joinAddresses = joinAddresses,
+ selectedJoinInfoType = selectedJoinInfoType,
+ onSelectedJoinInfoTypeChanged = { selectedJoinInfoType = it },
modifier = modifier
)
}
@@ -243,6 +263,9 @@ private fun PortraitContent(
hostInputAuthorityEnabled: Boolean,
maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit,
+ joinAddresses: Map<JoinInfoType, JoinAddress>,
+ selectedJoinInfoType: JoinInfoType,
+ onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -273,6 +296,9 @@ private fun PortraitContent(
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
isHosting = isHosting,
+ joinAddresses = joinAddresses,
+ selectedJoinInfoType = selectedJoinInfoType,
+ onSelectedJoinInfoTypeChanged = onSelectedJoinInfoTypeChanged,
modifier = Modifier
.padding(horizontal = DolphinTheme.scaffoldPadding),
)
@@ -299,10 +325,14 @@ private fun LandscapeContent(
hostInputAuthorityEnabled: Boolean,
maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit,
+ joinAddresses: Map<JoinInfoType, JoinAddress>,
+ selectedJoinInfoType: JoinInfoType,
+ onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
+ .padding(horizontal = DolphinTheme.scaffoldPadding)
) {
Chat(
messages = messages,
@@ -312,9 +342,10 @@ private fun LandscapeContent(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
- .padding(horizontal = DolphinTheme.scaffoldPadding)
)
+ Spacer(modifier = Modifier.width(16.dp))
+
Column(
modifier = Modifier
.weight(1f)
@@ -331,8 +362,10 @@ private fun LandscapeContent(
maxBuffer = maxBuffer,
onMaxBufferChanged = onMaxBufferChanged,
isHosting = isHosting,
+ joinAddresses = joinAddresses,
+ selectedJoinInfoType = selectedJoinInfoType,
+ onSelectedJoinInfoTypeChanged = onSelectedJoinInfoTypeChanged,
modifier = Modifier
- .padding(horizontal = DolphinTheme.scaffoldPadding)
)
if (isHosting) {
@@ -354,6 +387,9 @@ private fun PlayersAndSettings(
maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit,
isHosting: Boolean,
+ joinAddresses: Map<JoinInfoType, JoinAddress>,
+ selectedJoinInfoType: JoinInfoType,
+ onSelectedJoinInfoTypeChanged: (JoinInfoType) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -368,6 +404,16 @@ private fun PlayersAndSettings(
isHosting = isHosting,
)
+ if (isHosting) {
+ MenuSpacer()
+
+ JoinAddressSection(
+ joinAddresses = joinAddresses,
+ selectedType = selectedJoinInfoType,
+ onSelectedTypeChanged = onSelectedJoinInfoTypeChanged,
+ )
+ }
+
MenuSpacer()
OutlinedBox(
@@ -479,6 +525,7 @@ private fun Chat(
OutlinedBox(
onClick = { onShowBottomSheetChanged(true) },
label = { Text(stringResource(R.string.netplay_chat_label)) },
+ fadeContentTop = true,
modifier = modifier
) {
LazyColumn(
@@ -532,26 +579,16 @@ private fun GamePicker(
}
}
- Box(
+ ReadOnlyTextField(
+ value = game,
+ label = stringResource(R.string.netplay_game_label),
+ onClick = if (isHosting) {
+ { onShowGamePickerChanged(true) }
+ } else {
+ null
+ },
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
@@ -609,6 +646,150 @@ private fun GameGridItem(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun JoinAddressSection(
+ joinAddresses: Map<JoinInfoType, JoinAddress>,
+ selectedType: JoinInfoType,
+ onSelectedTypeChanged: (JoinInfoType) -> Unit,
+) {
+ val address = joinAddresses[selectedType] ?: joinAddresses.values.first()
+
+ @Suppress("UnusedBoxWithConstraintsScope")
+ BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
+ if (maxWidth > 392.dp) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ JoinInfoDropdown(
+ joinAddresses = joinAddresses,
+ selectedType = selectedType,
+ onSelectedTypeChanged = onSelectedTypeChanged,
+ modifier = Modifier.weight(0.39f),
+ )
+ AddressRow(
+ address = address,
+ modifier = Modifier.weight(0.61f),
+ )
+ }
+ } else {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ JoinInfoDropdown(
+ joinAddresses = joinAddresses,
+ selectedType = selectedType,
+ onSelectedTypeChanged = onSelectedTypeChanged,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ MenuSpacer()
+ AddressRow(
+ address = address,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun JoinInfoDropdown(
+ joinAddresses: Map<JoinInfoType, JoinAddress>,
+ selectedType: JoinInfoType,
+ onSelectedTypeChanged: (JoinInfoType) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ modifier = modifier,
+ ) {
+ OutlinedTextField(
+ value = stringResource(selectedType.labelId),
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(stringResource(R.string.netplay_host_address_label)) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+ modifier = Modifier
+ .menuAnchor(MenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth()
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ joinAddresses.keys.forEach { type ->
+ DropdownMenuItem(
+ text = { Text(stringResource(type.labelId)) },
+ onClick = {
+ onSelectedTypeChanged(type)
+ expanded = false
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun AddressRow(
+ address: JoinAddress,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+
+ ReadOnlyTextField(
+ value = when (address) {
+ is JoinAddress.Loading -> stringResource(R.string.netplay_address_loading)
+ is JoinAddress.Loaded -> address.address
+ is JoinAddress.Unknown -> stringResource(R.string.netplay_address_unknown)
+ },
+ label = stringResource(R.string.netplay_address_label),
+ onClick = when (address) {
+ is JoinAddress.Loaded -> {
+ {
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, address.address)
+ }
+ context.startActivity(Intent.createChooser(intent, null))
+ }
+ }
+
+ is JoinAddress.Unknown -> address.retry
+ is JoinAddress.Loading -> null
+ },
+ textStyle = if (address is JoinAddress.Loading) {
+ LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
+ } else {
+ null
+ },
+ trailingIcon = {
+ when (address) {
+ is JoinAddress.Loaded -> Icon(
+ imageVector = Icons.Filled.Share,
+ contentDescription = stringResource(R.string.netplay_address_share),
+ )
+
+ is JoinAddress.Unknown -> Icon(
+ imageVector = Icons.Filled.Refresh,
+ contentDescription = stringResource(R.string.netplay_address_retry),
+ )
+
+ is JoinAddress.Loading -> CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ strokeWidth = 2.dp,
+ )
+ }
+ },
+ modifier = modifier,
+ )
+}
+
/**
* 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.
@@ -981,6 +1162,10 @@ private fun PreviewNetplayScreen() {
onMaxBufferChanged = {},
saveTransferProgress = null,
gameDigestProgress = null,
+ joinAddresses = mapOf(
+ JoinInfoType.EXTERNAL to JoinAddress.Loaded("203.0.113.1:2626"),
+ JoinInfoType.LOCAL to JoinAddress.Loaded("192.168.1.5:2626"),
+ ),
// saveTransferProgress = SaveTransferProgress(
// title = "Title",
// totalSize = 1024L,
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 d25a345862..c5fa746346 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
@@ -17,8 +17,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
@@ -28,6 +31,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.google.android.material.color.MaterialColors
@@ -205,3 +209,35 @@ fun OutlinedBox(
}
}
}
+
+@Composable
+fun ReadOnlyTextField(
+ value: String,
+ label: String,
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+ trailingIcon: (@Composable () -> Unit)? = null,
+ textStyle: TextStyle? = null,
+) {
+ Box(modifier = modifier) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = {},
+ readOnly = true,
+ singleLine = true,
+ label = { Text(label) },
+ trailingIcon = trailingIcon,
+ textStyle = textStyle ?: LocalTextStyle.current,
+ modifier = Modifier.fillMaxWidth()
+ )
+ if (onClick != null) {
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .padding(top = 8.dp)
+ .clip(MaterialTheme.shapes.extraSmall)
+ .clickable(onClick = onClick)
+ )
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt
index 7bd2068ee2..231f15fda1 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/NetworkHelper.kt
@@ -75,4 +75,9 @@ object NetworkHelper {
0
}
}
+
+ @JvmStatic
+ fun getLocalIpString(): String? {
+ return getIPv4Link()?.address?.hostAddress
+ }
}
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 9c89d3aed7..20c78eddc5 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -1016,4 +1016,12 @@ It can efficiently compress both junk data and encrypted Wii data.
<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>
+ <string name="netplay_host_address_label">Join info</string>
+ <string name="netplay_address_label">Address</string>
+ <string name="netplay_address_type_external">External IP</string>
+ <string name="netplay_address_type_local">Local IP</string>
+ <string name="netplay_address_loading">Loading…</string>
+ <string name="netplay_address_unknown">Unknown</string>
+ <string name="netplay_address_share">Share address</string>
+ <string name="netplay_address_retry">Retry</string>
</resources>