summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-05-19 08:36:33 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:55 +0200
commitdfa3384281876b2fdc1c4d08ae1f4996033fbcae (patch)
tree2b05e2862a507f958f4386b197ab79dcccdb8dba /Source/Android/app/src/main/java
parentd9e2725e85eafecd9dac67dfdec8a3825d6933e1 (diff)
Netplay chat UI
Diffstat (limited to 'Source/Android/app/src/main/java')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt259
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/theme/DolphinTheme.kt88
2 files changed, 312 insertions, 35 deletions
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 e4089dae45..c3e4742d5d 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,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
- .fillMaxSize()
- .consumeWindowInsets(innerPadding)
- .verticalScroll(rememberScrollState())
- .padding(innerPadding)
+ .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()
) {
- 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"
+ ),
+)
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 24b0f99c0a..d24b3d834f 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
@@ -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,
+ )
+ )
+ }
+}