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..c94579d181 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, + 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, + 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, + 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" + ), +) 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, + ) + ) + } +} diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 903a414af3..bd1acb0bfb 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -996,4 +996,7 @@ It can efficiently compress both junk data and encrypted Wii data. Host code Port Netplay + Start + Chat + Send