Netplay chat UI

This commit is contained in:
Tom Pratt 2026-04-15 13:51:39 +02:00
parent 97279f24dd
commit d82a9242a1
3 changed files with 315 additions and 35 deletions

View File

@ -2,15 +2,20 @@
package org.dolphinemu.dolphinemu.features.netplay.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@ -21,13 +26,22 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -36,6 +50,9 @@ 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
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -50,29 +67,160 @@ fun NetplayScreen(
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
},
)
},
) { innerPadding ->
Column(
val modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(innerPadding)
.padding(innerPadding)
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
LandscapeContent(
players = players,
modifier = modifier
)
} else {
PortraitContent(
players = players,
modifier = modifier
)
}
}
}
@Composable
private fun PortraitContent(
players: List<Player>,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
) {
Chat(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.3f)
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
MenuSpacer()
PLayersAndSettings(
players = players,
modifier = Modifier
.weight(1f)
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
}
}
@Composable
private fun LandscapeContent(
players: List<Player>,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
) {
Chat(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
PLayersAndSettings(
players = players,
modifier = Modifier
.weight(1f)
.padding(horizontal = DolphinTheme.scaffoldPadding)
)
}
}
@Composable
private fun PLayersAndSettings(
players: List<Player>,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
) {
PlayersTable(
rows = buildList {
add(listOf("Player", "Ping", "Mapping"))
addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
repeat(4 - players.size) { add(listOf("", "", "")) }
},
modifier = Modifier
.fillMaxWidth()
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Chat(
modifier: Modifier,
) {
var showBottomSheet by remember { mutableStateOf(false) }
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = { showBottomSheet = false },
sheetState = bottomSheetState,
modifier = Modifier
.statusBarsPadding()
) {
LazyColumn(
reverseLayout = true,
contentPadding = PaddingValues(bottom = 4.dp),
modifier = Modifier
.weight(1f)
.padding(horizontal = DolphinTheme.scaffoldPadding)
) {
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier
.weight(1f)
)
TextButton(
onClick = {},
) {
Text(stringResource(R.string.netplay_chat_send))
}
}
}
}
OutlinedBox(
onClick = { showBottomSheet = true },
label = { Text(stringResource(R.string.netplay_chat_label)) },
modifier = modifier
) {
LazyColumn(
reverseLayout = true,
userScrollEnabled = false,
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()
)
}
}
}
@ -137,25 +285,66 @@ private fun PlayersTable(
@Preview
@Composable
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"
),
),
)
PreviewTheme(darkTheme = false) {
NetplayScreen(
onBackClicked = {},
players = previewPlayers,
)
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun NetplayScreenDarkPreview() {
PreviewTheme(darkTheme = true) {
NetplayScreen(
onBackClicked = {},
players = previewPlayers,
)
}
}
@Preview(widthDp = 891, heightDp = 411)
@Composable
private fun LandscapeNetplayScreenPreview() {
PreviewTheme(darkTheme = false) {
NetplayScreen(
onBackClicked = {},
players = previewPlayers,
)
}
}
@Preview(
widthDp = 891,
heightDp = 411,
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
private fun LandscapeNetplayScreenDarkPreview() {
PreviewTheme(darkTheme = true) {
NetplayScreen(
onBackClicked = {},
players = previewPlayers,
)
}
}
private val previewPlayers = 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"
),
)

View File

@ -4,18 +4,31 @@ package org.dolphinemu.dolphinemu.ui.theme
import android.content.Context
import androidx.annotation.AttrRes
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
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.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
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.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.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.google.android.material.color.MaterialColors
import androidx.appcompat.R as AppCompatR
@ -37,6 +50,17 @@ fun DolphinTheme(content: @Composable () -> Unit) {
)
}
@Composable
fun PreviewTheme(
darkTheme: Boolean,
content: @Composable () -> Unit,
) {
MaterialTheme(
colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme(),
content = content
)
}
private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
fun attr(@AttrRes attr: Int) = Color(MaterialColors.getColor(this, attr, 0))
@ -107,3 +131,67 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
@Composable
fun MenuSpacer() = Spacer(modifier = Modifier.height(16.dp))
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OutlinedBox(
onClick: () -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = modifier
.padding(top = 8.dp)
) {
val interactionSource = remember { MutableInteractionSource() }
OutlinedTextFieldDefaults.DecorationBox(
value = "chatText",
innerTextField = {
Box(
modifier = Modifier
.fillMaxSize()
) {
content()
Box(
modifier = Modifier
.fillMaxWidth()
.height(16.dp)
.background(
Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.surface,
Color.Transparent
)
)
)
)
}
},
enabled = true,
singleLine = false,
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 0.dp),
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
label = { label() },
container = {
OutlinedTextFieldDefaults.Container(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = OutlinedTextFieldDefaults.colors(),
)
}
)
Box(
modifier = Modifier
.fillMaxSize()
.clip(MaterialTheme.shapes.extraSmall)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = onClick,
)
)
}
}

View File

@ -996,4 +996,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_host_code_label">Host code</string>
<string name="netplay_port_label">Port</string>
<string name="netplay_title">Netplay</string>
<string name="netplay_start">Start</string>
<string name="netplay_chat_label">Chat</string>
<string name="netplay_chat_send">Send</string>
</resources>