feat: add 2 columns players lists

This commit is contained in:
Leonardo Murça 2025-07-19 14:27:18 -03:00
parent fc2c939e0b
commit ab727dd061
3 changed files with 181 additions and 38 deletions

View file

@ -188,7 +188,7 @@ private fun Modifier.topBorder(color: Color, thickness: Dp): Modifier = this.the
}) })
private fun getOrDefaultOpponents(opponents: List<Opponent>): Pair<Opponent, Opponent> { private fun getOrDefaultOpponents(opponents: List<Opponent>): Pair<Opponent, Opponent> {
val default = Opponent(id = -1, name = "A ser definido", imageUrl = "") val default = Opponent(id = -1, name = "A definir", imageUrl = "")
return when { return when {
opponents.size >= 2 -> opponents[0] to opponents[1] opponents.size >= 2 -> opponents[0] to opponents[1]
opponents.size == 1 -> opponents[0] to default opponents.size == 1 -> opponents[0] to default

View file

@ -2,16 +2,23 @@ package xyz.leomurca.csgomatches.ui.screens.matchdetails
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.size 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -19,16 +26,26 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState 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.Alignment
import androidx.compose.ui.Modifier 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.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign 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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import xyz.leomurca.csgomatches.R import xyz.leomurca.csgomatches.R
import xyz.leomurca.csgomatches.domain.model.MatchStatus 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.domain.model.Team
import xyz.leomurca.csgomatches.ui.components.LoadingIndicator import xyz.leomurca.csgomatches.ui.components.LoadingIndicator
import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute import xyz.leomurca.csgomatches.ui.navigation.MatchDetailsRoute
@ -48,12 +65,12 @@ fun MatchDetailsScreen(
if (matchDetails.leftOpponentId != -1L) { if (matchDetails.leftOpponentId != -1L) {
viewModel.loadTeam(matchDetails.leftOpponentId.toString(), isLeft = true) viewModel.loadTeam(matchDetails.leftOpponentId.toString(), isLeft = true)
} else { } else {
viewModel.updateTeamToDefault(isLeft = true) viewModel.applyPlaceholderTeamToSide(isLeft = true)
} }
if (matchDetails.rightOpponentId != -1L) { if (matchDetails.rightOpponentId != -1L) {
viewModel.loadTeam(matchDetails.rightOpponentId.toString(), isLeft = false) viewModel.loadTeam(matchDetails.rightOpponentId.toString(), isLeft = false)
} else { } else {
viewModel.updateTeamToDefault(isLeft = false) viewModel.applyPlaceholderTeamToSide(isLeft = false)
} }
} }
@ -71,13 +88,130 @@ fun MatchDetailsScreen(
value.leftTeam != null && value.rightTeam != null -> Column { value.leftTeam != null && value.rightTeam != null -> Column {
MatchupRow(leftTeam = value.leftTeam, rightTeam = value.rightTeam) MatchupRow(leftTeam = value.leftTeam, rightTeam = value.rightTeam)
ScheduleRow(matchDetails.scheduleString, matchDetails.matchStatus) ScheduleRow(matchDetails.scheduleString, matchDetails.matchStatus)
DualPlayerLists(value.leftTeam.players, value.rightTeam.players)
} }
} }
} }
} }
@Composable @Composable
fun ScheduleRow(scheduleString: String, matchStatus: MatchStatus) { fun DualPlayerLists(
leftPlayers: List<Player>,
rightPlayers: List<Player>,
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( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -115,7 +249,7 @@ private fun MatchupRow(leftTeam: Team, rightTeam: Team) {
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
Text( Text(
leftTeam.name ?: "A ser definido", leftTeam.name ?: "A definir",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = White, color = White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -141,7 +275,7 @@ private fun MatchupRow(leftTeam: Team, rightTeam: Team) {
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
Text( Text(
rightTeam.name ?: "A ser definido", rightTeam.name ?: "A definir",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = White, color = White,
textAlign = TextAlign.Center, 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
}

View file

@ -8,14 +8,16 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.leomurca.csgomatches.domain.model.Player
import xyz.leomurca.csgomatches.domain.model.Resource import xyz.leomurca.csgomatches.domain.model.Resource
import xyz.leomurca.csgomatches.domain.model.Team import xyz.leomurca.csgomatches.domain.model.Team
import xyz.leomurca.csgomatches.domain.repository.MatchRepository import xyz.leomurca.csgomatches.domain.repository.MatchRepository
import javax.inject.Inject import javax.inject.Inject
import kotlin.String
@HiltViewModel @HiltViewModel
class MatchDetailsViewModel @Inject constructor( class MatchDetailsViewModel @Inject constructor(
private val matchRepository: MatchRepository, private val matchRepository: MatchRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(TeamUiState()) private val _uiState = MutableStateFlow(TeamUiState())
@ -23,15 +25,11 @@ class MatchDetailsViewModel @Inject constructor(
fun loadTeam(teamId: String, isLeft: Boolean) { fun loadTeam(teamId: String, isLeft: Boolean) {
viewModelScope.launch { viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, errorMessage = null) } setLoading()
when (val result = matchRepository.teamDetails(teamId)) { when (val result = matchRepository.teamDetails(teamId)) {
is Resource.Success -> { is Resource.Success -> {
val team = result.data.firstOrNull() val team = teamOrWithDefaultPlayers(result.data.firstOrNull())
_uiState.update { updateTeam(team, isLeft)
if (isLeft) it.copy(leftTeam = team, isLoading = false)
else it.copy(rightTeam = team, isLoading = false)
}
} }
is Resource.Error -> { is Resource.Error -> {
@ -43,33 +41,36 @@ class MatchDetailsViewModel @Inject constructor(
} }
} }
fun updateTeamToDefault(isLeft: Boolean) { fun applyPlaceholderTeamToSide(isLeft: Boolean) {
_uiState.update { it.copy(isLoading = true, errorMessage = null) } setLoading()
_uiState.update { updateTeam(teamPlaceholder(), isLeft)
if (isLeft) { }
it.copy(
leftTeam = Team( private fun teamOrWithDefaultPlayers(team: Team?): Team? {
id = -1, return team?.let {
name = null, if (it.players.isEmpty()) it.copy(players = List(5) { playerPlaceholder() }) else it
imageUrl = null,
players = emptyList()
),
isLoading = false
)
} else {
it.copy(
rightTeam = Team(
id = -1,
name = null,
imageUrl = null,
players = emptyList()
),
isLoading = false
)
}
} }
} }
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( data class TeamUiState(
val leftTeam: Team? = null, val leftTeam: Team? = null,
val rightTeam: Team? = null, val rightTeam: Team? = null,