diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt index 0e9abfe..64516c8 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/components/MatchCard.kt @@ -188,7 +188,7 @@ private fun Modifier.topBorder(color: Color, thickness: Dp): Modifier = this.the }) private fun getOrDefaultOpponents(opponents: List): Pair { - val default = Opponent(id = -1, name = "A ser definido", imageUrl = "") + val default = Opponent(id = -1, name = "A definir", imageUrl = "") return when { opponents.size >= 2 -> opponents[0] to opponents[1] opponents.size == 1 -> opponents[0] to default diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsScreen.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsScreen.kt index 0ada304..efb4c0a 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsScreen.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsScreen.kt @@ -2,16 +2,23 @@ package xyz.leomurca.csgomatches.ui.screens.matchdetails import androidx.compose.foundation.background 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.Spacer +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -19,16 +26,26 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter import xyz.leomurca.csgomatches.R import xyz.leomurca.csgomatches.domain.model.MatchStatus +import xyz.leomurca.csgomatches.domain.model.Player import xyz.leomurca.csgomatches.domain.model.Team import xyz.leomurca.csgomatches.ui.components.LoadingIndicator import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute @@ -48,12 +65,12 @@ fun MatchDetailsScreen( if (matchDetails.leftOpponentId != -1L) { viewModel.loadTeam(matchDetails.leftOpponentId.toString(), isLeft = true) } else { - viewModel.updateTeamToDefault(isLeft = true) + viewModel.applyPlaceholderTeamToSide(isLeft = true) } if (matchDetails.rightOpponentId != -1L) { viewModel.loadTeam(matchDetails.rightOpponentId.toString(), isLeft = false) } else { - viewModel.updateTeamToDefault(isLeft = false) + viewModel.applyPlaceholderTeamToSide(isLeft = false) } } @@ -71,13 +88,130 @@ fun MatchDetailsScreen( value.leftTeam != null && value.rightTeam != null -> Column { MatchupRow(leftTeam = value.leftTeam, rightTeam = value.rightTeam) ScheduleRow(matchDetails.scheduleString, matchDetails.matchStatus) + DualPlayerLists(value.leftTeam.players, value.rightTeam.players) } } } } @Composable -fun ScheduleRow(scheduleString: String, matchStatus: MatchStatus) { +fun DualPlayerLists( + leftPlayers: List, + rightPlayers: List, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 0.dp, top = 16.dp, end = 0.dp, bottom = 60.dp) + ) { + // Left team + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + ) { + items(leftPlayers) { player -> + PlayerCard(player = player, isLeft = true) + } + } + + // Right team + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(start = 4.dp) + ) { + items(rightPlayers) { player -> + PlayerCard(player = player, isLeft = false) + } + } + } +} + +@Composable +private fun PlayerCard(player: Player, isLeft: Boolean) { + val roundedCornerShape = if (isLeft) RoundedCornerShape(topEnd = 12.dp, bottomEnd = 12.dp) + else RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + .background(MaterialTheme.colorScheme.surface, roundedCornerShape) + .padding( + top = 8.dp, + bottom = 8.dp, + start = if (isLeft) 4.dp else 12.dp, + end = if (isLeft) 12.dp else 4.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = if (isLeft) Arrangement.End else Arrangement.Start + ) { + if (isLeft) { + PlayerInfo(player, Modifier.width(100.dp), alignEnd = true) + Spacer(modifier = Modifier.width(12.dp)) + PlayerAvatar(size = 48.dp, player.imageUrl, player.nickName) + } else { + PlayerAvatar(size = 48.dp, player.imageUrl, player.nickName) + Spacer(modifier = Modifier.width(12.dp)) + PlayerInfo(player, Modifier.width(100.dp), alignEnd = false) + } + } +} + +@Composable +private fun PlayerAvatar(size: Dp, imageUrl: String?, nickName: String?) { + var isLoading by remember { mutableStateOf(true) } + Box( + modifier = Modifier + .absoluteOffset(y = (-12).dp) + .size(size) + .clip(RoundedCornerShape(8.dp)) + .background(Color.LightGray) + ) { + AsyncImage( + model = imageUrl, + contentDescription = "$nickName logo", + contentScale = ContentScale.Crop, + onState = { isLoading = it is AsyncImagePainter.State.Loading }, + modifier = Modifier.fillMaxSize() + ) + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp).align(Alignment.Center), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.background + ) + } + } +} + +@Composable +private fun PlayerInfo(player: Player, modifier: Modifier = Modifier, alignEnd: Boolean) { + Column( + modifier = modifier, + horizontalAlignment = if (alignEnd) Alignment.End else Alignment.Start + ) { + Text( + text = player.nickName ?: "A definir", + color = Color.White, + style = MaterialTheme.typography.headlineLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = playerFullNameOrEmpty(player.firstName, player.lastName), + color = Color.Gray, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun ScheduleRow(scheduleString: String, matchStatus: MatchStatus) { Row( Modifier .fillMaxWidth() @@ -115,7 +249,7 @@ private fun MatchupRow(leftTeam: Team, rightTeam: Team) { contentScale = ContentScale.Fit ) Text( - leftTeam.name ?: "A ser definido", + leftTeam.name ?: "A definir", style = MaterialTheme.typography.labelMedium, color = White, textAlign = TextAlign.Center, @@ -141,7 +275,7 @@ private fun MatchupRow(leftTeam: Team, rightTeam: Team) { contentScale = ContentScale.Fit ) Text( - rightTeam.name ?: "A ser definido", + rightTeam.name ?: "A definir", style = MaterialTheme.typography.labelMedium, color = White, textAlign = TextAlign.Center, @@ -174,3 +308,11 @@ private fun TopBar(leagueAndSerieName: String, onBackClick: () -> Unit) { ) } } + +private fun playerFullNameOrEmpty(firstName: String?, lastName: String?): String { + val fullName = listOfNotNull(firstName?.trim(), lastName?.trim()) + .filter { it.isNotBlank() } + .joinToString(" ") + + return if (fullName.isBlank()) "" else fullName +} diff --git a/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsViewModel.kt b/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsViewModel.kt index b03aa6f..d9b0257 100644 --- a/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsViewModel.kt +++ b/app/src/main/java/xyz/leomurca/csgomatches/ui/screens/matchdetails/MatchDetailsViewModel.kt @@ -8,14 +8,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import xyz.leomurca.csgomatches.domain.model.Player import xyz.leomurca.csgomatches.domain.model.Resource import xyz.leomurca.csgomatches.domain.model.Team import xyz.leomurca.csgomatches.domain.repository.MatchRepository import javax.inject.Inject +import kotlin.String @HiltViewModel class MatchDetailsViewModel @Inject constructor( - private val matchRepository: MatchRepository, + private val matchRepository: MatchRepository ) : ViewModel() { private val _uiState = MutableStateFlow(TeamUiState()) @@ -23,15 +25,11 @@ class MatchDetailsViewModel @Inject constructor( fun loadTeam(teamId: String, isLeft: Boolean) { viewModelScope.launch { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - + setLoading() when (val result = matchRepository.teamDetails(teamId)) { is Resource.Success -> { - val team = result.data.firstOrNull() - _uiState.update { - if (isLeft) it.copy(leftTeam = team, isLoading = false) - else it.copy(rightTeam = team, isLoading = false) - } + val team = teamOrWithDefaultPlayers(result.data.firstOrNull()) + updateTeam(team, isLeft) } is Resource.Error -> { @@ -43,33 +41,36 @@ class MatchDetailsViewModel @Inject constructor( } } - fun updateTeamToDefault(isLeft: Boolean) { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - _uiState.update { - if (isLeft) { - it.copy( - leftTeam = Team( - id = -1, - name = null, - imageUrl = null, - players = emptyList() - ), - isLoading = false - ) - } else { - it.copy( - rightTeam = Team( - id = -1, - name = null, - imageUrl = null, - players = emptyList() - ), - isLoading = false - ) - } + fun applyPlaceholderTeamToSide(isLeft: Boolean) { + setLoading() + updateTeam(teamPlaceholder(), isLeft) + } + + private fun teamOrWithDefaultPlayers(team: Team?): Team? { + return team?.let { + if (it.players.isEmpty()) it.copy(players = List(5) { playerPlaceholder() }) else it } } + private fun updateTeam(team: Team?, isLeft: Boolean) { + _uiState.update { + if (isLeft) it.copy(leftTeam = team, isLoading = false) + else it.copy(rightTeam = team, isLoading = false) + } + } + + private fun setLoading() { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + } + + private fun playerPlaceholder() = Player( + id = -1, nickName = "A definir", firstName = "", lastName = "", imageUrl = null + ) + + private fun teamPlaceholder() = Team( + id = -1, name = null, imageUrl = null, players = List(5) { playerPlaceholder() } + ) + data class TeamUiState( val leftTeam: Team? = null, val rightTeam: Team? = null,