summaryrefslogtreecommitdiff
path: root/Source/Android/app
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-04-10 11:30:59 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:55 +0200
commitd9e2725e85eafecd9dac67dfdec8a3825d6933e1 (patch)
tree312838a9555a3d7aac60357601ff3335eb7628e1 /Source/Android/app
parentaf2fda564954b975115b212221aa13f0308fe406 (diff)
Implement player list
Diffstat (limited to 'Source/Android/app')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt19
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt13
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt119
5 files changed, 157 insertions, 3 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
index 9b19c9c2fd..ceda995051 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
@@ -5,11 +5,17 @@ package org.dolphinemu.dolphinemu.features.netplay
import androidx.annotation.Keep
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType
+import org.dolphinemu.dolphinemu.features.netplay.model.Player
object Netplay {
@Keep
@@ -27,6 +33,12 @@ object Netplay {
private val _connectionErrors = Channel<String>(Channel.BUFFERED)
val connectionErrors = _connectionErrors.receiveAsFlow()
+ private val _players = MutableSharedFlow<List<Player>>(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val players = _players.asSharedFlow().distinctUntilChanged()
+
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
netPlayClientPointer = Join()
val isConnected = netPlayClientPointer != 0L && isClientConnected()
@@ -48,6 +60,7 @@ object Netplay {
releaseNetplayClient()
}
+ @OptIn(ExperimentalCoroutinesApi::class)
private fun releaseNetplayClient() {
if (netPlayClientPointer != 0L) {
ReleaseNetplayClient()
@@ -55,6 +68,7 @@ object Netplay {
}
_launchGame.flush()
_connectionErrors.flush()
+ _players.resetReplayCache()
}
@JvmStatic
@@ -79,6 +93,11 @@ object Netplay {
_connectionErrors.trySend(message)
}
+ @JvmStatic
+ fun onUpdate(players: Array<Player>) {
+ _players.tryEmit(players.toList())
+ }
+
// Settings
@JvmStatic
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 cf941aa01f..942dc0d0dc 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
@@ -3,11 +3,14 @@
package org.dolphinemu.dolphinemu.features.netplay.model
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.features.netplay.Netplay
@@ -17,6 +20,9 @@ class NetplayViewModel : ViewModel() {
private val _goBack = Channel<Unit>(CONFLATED)
val goBack = _goBack.receiveAsFlow()
+ val players = Netplay.players
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
init {
if (!Netplay.isClientConnected()) {
_goBack.trySend(Unit)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt
new file mode 100644
index 0000000000..70c228d1c9
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/Player.kt
@@ -0,0 +1,13 @@
+// Copyright 2003 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+data class Player(
+ val pid: Int,
+ val name: String,
+ val revision: String,
+ val ping: Int,
+ val isHost: Boolean,
+ val mapping: String,
+) \ No newline at end of file
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 7f199bd45e..5a91ae5b91 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
@@ -8,6 +8,7 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.collectAsState
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.flowWithLifecycle
@@ -15,7 +16,6 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.dolphinemu.dolphinemu.activities.EmulationActivity
-import org.dolphinemu.dolphinemu.features.netplay.Netplay
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
@@ -44,6 +44,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
DolphinTheme {
NetplayScreen(
onBackClicked = { finish() },
+ players = viewModel.players.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 de3b89d6c1..e4089dae45 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,23 +2,46 @@
package org.dolphinemu.dolphinemu.features.netplay.ui
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import org.dolphinemu.dolphinemu.R
+import org.dolphinemu.dolphinemu.features.netplay.model.Player
+import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NetplayScreen(
onBackClicked: () -> Unit,
+ players: List<Player>,
) {
Scaffold(
topBar = {
@@ -34,7 +57,81 @@ fun NetplayScreen(
},
)
},
- ) { _ -> }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .consumeWindowInsets(innerPadding)
+ .verticalScroll(rememberScrollState())
+ .padding(innerPadding)
+ .padding(horizontal = DolphinTheme.scaffoldPadding)
+ ) {
+ PlayersTable(
+ rows = buildList {
+ add(listOf("Player", "Ping", "Mapping"))
+ addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
+ },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
+
+/**
+ * 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.
+ * The first row is treated as the column titles.
+ */
+@Composable
+private fun PlayersTable(
+ rows: List<List<String>>,
+ modifier: Modifier = Modifier,
+) {
+ rows.zipWithNext { a, b -> if (a.size != b.size) throw IllegalArgumentException("Rows must all contain the same number of elements.") }
+ val maxWidths = remember { List(rows.first().size) { mutableIntStateOf(0) } }
+ val density = LocalDensity.current
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ modifier = modifier
+ ) {
+ rows.forEachIndexed { rowIndex, row ->
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ row.forEachIndexed { itemIndex, text ->
+ Box(
+ modifier = Modifier
+ .then(
+ when {
+ itemIndex == 0 -> Modifier.weight(1f)
+
+ maxWidths[itemIndex].intValue > 0 -> Modifier
+ .width(with(density) { maxWidths[itemIndex].intValue.toDp() })
+
+ else -> Modifier
+ }
+ )
+ .onGloballyPositioned { coordinates ->
+ val width = coordinates.size.width
+ if (width > maxWidths[itemIndex].intValue) {
+ maxWidths[itemIndex].intValue = width
+ }
+ }
+ ) {
+ Text(
+ text = text,
+ fontWeight = if (rowIndex == 0) FontWeight.Medium else FontWeight.Normal,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+ }
+ if (rowIndex == 0) {
+ HorizontalDivider()
+ }
+ }
+ }
}
@Preview
@@ -42,5 +139,23 @@ fun NetplayScreen(
private fun NetplayScreenPreview() {
NetplayScreen(
onBackClicked = {},
+ players = listOf(
+ Player(
+ pid = 1,
+ name = "Player 1",
+ revision = "123",
+ ping = 2,
+ isHost = true,
+ mapping = "m1"
+ ),
+ Player(
+ pid = 2,
+ name = "Player 2",
+ revision = "123",
+ ping = 23,
+ isHost = false,
+ mapping = "m2"
+ ),
+ ),
)
-} \ No newline at end of file
+}