feat: add details screen with navigation and initial structure
This commit is contained in:
parent
c5d1bbcbed
commit
07d20aba86
19 changed files with 312 additions and 82 deletions
|
@ -5,6 +5,7 @@ import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||||
import xyz.leomurca.csgomatches.data.model.OpponentDto
|
import xyz.leomurca.csgomatches.data.model.OpponentDto
|
||||||
import xyz.leomurca.csgomatches.data.model.OpponentRecord
|
import xyz.leomurca.csgomatches.data.model.OpponentRecord
|
||||||
import xyz.leomurca.csgomatches.data.model.SerieDto
|
import xyz.leomurca.csgomatches.data.model.SerieDto
|
||||||
|
import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
||||||
import xyz.leomurca.csgomatches.data.source.MatchDataSource
|
import xyz.leomurca.csgomatches.data.source.MatchDataSource
|
||||||
import xyz.leomurca.csgomatches.domain.model.Resource
|
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||||
|
|
||||||
|
@ -151,4 +152,8 @@ class MatchLocalDataSource : MatchDataSource {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun teamDetails(teamId: String): Resource<List<TeamDetailsDto>> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
package xyz.leomurca.csgomatches.data.mapper
|
package xyz.leomurca.csgomatches.data.mapper
|
||||||
|
|
||||||
import xyz.leomurca.csgomatches.data.model.MatchDto
|
import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||||
|
import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
||||||
import xyz.leomurca.csgomatches.domain.model.League
|
import xyz.leomurca.csgomatches.domain.model.League
|
||||||
import xyz.leomurca.csgomatches.domain.model.Match
|
import xyz.leomurca.csgomatches.domain.model.Match
|
||||||
import xyz.leomurca.csgomatches.domain.model.MatchStatus
|
import xyz.leomurca.csgomatches.domain.model.MatchStatus
|
||||||
import xyz.leomurca.csgomatches.domain.model.Opponent
|
import xyz.leomurca.csgomatches.domain.model.Opponent
|
||||||
|
import xyz.leomurca.csgomatches.domain.model.Player
|
||||||
import xyz.leomurca.csgomatches.domain.model.Serie
|
import xyz.leomurca.csgomatches.domain.model.Serie
|
||||||
|
import xyz.leomurca.csgomatches.domain.model.Team
|
||||||
|
|
||||||
fun MatchDto.toDomain(): Match {
|
fun MatchDto.toDomain(): Match {
|
||||||
return Match(
|
return Match(
|
||||||
|
@ -31,4 +34,21 @@ fun MatchDto.toDomain(): Match {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String?.toMatchStatus() =
|
private fun String?.toMatchStatus() =
|
||||||
if (this == "running") MatchStatus.LIVE else MatchStatus.SCHEDULED
|
if (this == "running") MatchStatus.LIVE else MatchStatus.SCHEDULED
|
||||||
|
|
||||||
|
fun TeamDetailsDto.toDomain(): Team {
|
||||||
|
return Team(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
players = players.map {
|
||||||
|
Player(
|
||||||
|
id = it.id,
|
||||||
|
nickName = it.name,
|
||||||
|
firstName = it.firstName,
|
||||||
|
lastName = it.lastName,
|
||||||
|
imageUrl = it.imageUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package xyz.leomurca.csgomatches.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerDto(
|
||||||
|
val id: Long,
|
||||||
|
val name: String?,
|
||||||
|
@SerialName("first_name")
|
||||||
|
val firstName: String?,
|
||||||
|
@SerialName("last_name")
|
||||||
|
val lastName: String?,
|
||||||
|
@SerialName("image_url")
|
||||||
|
val imageUrl: String?
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
package xyz.leomurca.csgomatches.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TeamDetailsDto(
|
||||||
|
val id: Long,
|
||||||
|
val name: String?,
|
||||||
|
@SerialName("image_url")
|
||||||
|
val imageUrl: String?,
|
||||||
|
val players: List<PlayerDto>
|
||||||
|
)
|
|
@ -3,6 +3,7 @@ package xyz.leomurca.csgomatches.data.remote
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import xyz.leomurca.csgomatches.data.model.ErrorDto
|
import xyz.leomurca.csgomatches.data.model.ErrorDto
|
||||||
import xyz.leomurca.csgomatches.data.model.MatchDto
|
import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||||
|
import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
||||||
import xyz.leomurca.csgomatches.data.source.MatchDataSource
|
import xyz.leomurca.csgomatches.data.source.MatchDataSource
|
||||||
import xyz.leomurca.csgomatches.domain.model.Resource
|
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||||
|
|
||||||
|
@ -25,4 +26,20 @@ class MatchRemoteDataSourceImpl(
|
||||||
Resource.Error(e.message.toString())
|
Resource.Error(e.message.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun teamDetails(teamId: String): Resource<List<TeamDetailsDto>> {
|
||||||
|
return try {
|
||||||
|
val response = matchesApiService.teamDetails(teamId)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
Resource.Success(response.body() ?: throw Exception("Empty response body"))
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: ""
|
||||||
|
val networkError = json.decodeFromString<ErrorDto>(errorBody)
|
||||||
|
Resource.Error(networkError.message)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Resource.Error(e.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
import xyz.leomurca.csgomatches.data.model.MatchDto
|
import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||||
|
import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
||||||
|
|
||||||
interface MatchesApiService {
|
interface MatchesApiService {
|
||||||
|
|
||||||
|
@ -13,4 +14,9 @@ interface MatchesApiService {
|
||||||
@Query("finished") finished: Boolean = false,
|
@Query("finished") finished: Boolean = false,
|
||||||
@Query("sort") sort: String = "begin_at",
|
@Query("sort") sort: String = "begin_at",
|
||||||
): Response<List<MatchDto>>
|
): Response<List<MatchDto>>
|
||||||
|
|
||||||
|
@GET("teams")
|
||||||
|
suspend fun teamDetails(
|
||||||
|
@Query("filter[id]") id: String
|
||||||
|
): Response<List<TeamDetailsDto>>
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ import xyz.leomurca.csgomatches.di.AppDispatchers
|
||||||
import xyz.leomurca.csgomatches.di.Dispatcher
|
import xyz.leomurca.csgomatches.di.Dispatcher
|
||||||
import xyz.leomurca.csgomatches.domain.model.Match
|
import xyz.leomurca.csgomatches.domain.model.Match
|
||||||
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.repository.MatchRepository
|
import xyz.leomurca.csgomatches.domain.repository.MatchRepository
|
||||||
|
|
||||||
class MatchRepositoryImpl(
|
class MatchRepositoryImpl(
|
||||||
|
@ -22,4 +23,13 @@ class MatchRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun teamDetails(teamId: String): Resource<List<Team>> {
|
||||||
|
return withContext(ioDispatcher) {
|
||||||
|
when (val result = matchRemoteDataSource.teamDetails(teamId)) {
|
||||||
|
is Resource.Success -> Resource.Success(result.data.map { it.toDomain() })
|
||||||
|
is Resource.Error -> Resource.Error(result.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
package xyz.leomurca.csgomatches.data.source
|
package xyz.leomurca.csgomatches.data.source
|
||||||
|
|
||||||
import xyz.leomurca.csgomatches.data.model.MatchDto
|
import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||||
|
import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
||||||
import xyz.leomurca.csgomatches.domain.model.Resource
|
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||||
|
|
||||||
interface MatchDataSource {
|
interface MatchDataSource {
|
||||||
suspend fun upcomingMatches(): Resource<List<MatchDto>>
|
suspend fun upcomingMatches(): Resource<List<MatchDto>>
|
||||||
|
suspend fun teamDetails(teamId: String): Resource<List<TeamDetailsDto>>
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ package xyz.leomurca.csgomatches.domain.model
|
||||||
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
|
||||||
data class Match(
|
data class Match(
|
||||||
val beginAt: ZonedDateTime?,
|
val beginAt: ZonedDateTime?,
|
||||||
val opponents: List<Opponent>,
|
val opponents: List<Opponent>,
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package xyz.leomurca.csgomatches.domain.model
|
||||||
|
|
||||||
|
|
||||||
|
data class Player(
|
||||||
|
val id: Long,
|
||||||
|
val nickName: String?,
|
||||||
|
val firstName: String?,
|
||||||
|
val lastName: String?,
|
||||||
|
val imageUrl: String?
|
||||||
|
)
|
|
@ -0,0 +1,8 @@
|
||||||
|
package xyz.leomurca.csgomatches.domain.model
|
||||||
|
|
||||||
|
data class Team(
|
||||||
|
val id: Long,
|
||||||
|
val name: String?,
|
||||||
|
val imageUrl: String?,
|
||||||
|
val players: List<Player>
|
||||||
|
)
|
|
@ -2,8 +2,11 @@ package xyz.leomurca.csgomatches.domain.repository
|
||||||
|
|
||||||
import xyz.leomurca.csgomatches.domain.model.Match
|
import xyz.leomurca.csgomatches.domain.model.Match
|
||||||
import xyz.leomurca.csgomatches.domain.model.Resource
|
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||||
|
import xyz.leomurca.csgomatches.domain.model.Team
|
||||||
|
|
||||||
interface MatchRepository {
|
interface MatchRepository {
|
||||||
|
|
||||||
suspend fun upcomingMatches(): Resource<List<Match>>
|
suspend fun upcomingMatches(): Resource<List<Match>>
|
||||||
|
|
||||||
|
suspend fun teamDetails(teamId: String): Resource<List<Team>>
|
||||||
}
|
}
|
|
@ -33,6 +33,7 @@ import xyz.leomurca.csgomatches.domain.model.League
|
||||||
import xyz.leomurca.csgomatches.domain.model.Match
|
import xyz.leomurca.csgomatches.domain.model.Match
|
||||||
import xyz.leomurca.csgomatches.domain.model.MatchStatus
|
import xyz.leomurca.csgomatches.domain.model.MatchStatus
|
||||||
import xyz.leomurca.csgomatches.domain.model.Opponent
|
import xyz.leomurca.csgomatches.domain.model.Opponent
|
||||||
|
import xyz.leomurca.csgomatches.ui.navigation.DetailsRoute
|
||||||
import xyz.leomurca.csgomatches.ui.theme.LiveRed
|
import xyz.leomurca.csgomatches.ui.theme.LiveRed
|
||||||
import xyz.leomurca.csgomatches.ui.theme.White_20
|
import xyz.leomurca.csgomatches.ui.theme.White_20
|
||||||
import xyz.leomurca.csgomatches.ui.theme.White_50
|
import xyz.leomurca.csgomatches.ui.theme.White_50
|
||||||
|
@ -42,23 +43,35 @@ import java.time.ZonedDateTime
|
||||||
@Composable
|
@Composable
|
||||||
fun MatchCard(
|
fun MatchCard(
|
||||||
match: Match,
|
match: Match,
|
||||||
|
onTapCard: (DetailsRoute) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier.fillMaxWidth()) {
|
Box(modifier = modifier.fillMaxWidth()) {
|
||||||
|
val (leftOpponent, rightOpponent) = getOrDefaultOpponents(match.opponents)
|
||||||
|
val leagueAndSerieName = "${match.league.name} + ${match.serie.name}"
|
||||||
|
val scheduleConfig = match.status.scheduleConfigFor(match.beginAt)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), onClick = {
|
||||||
shape = RoundedCornerShape(16.dp),
|
onTapCard(
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
DetailsRoute(
|
||||||
|
leftOpponentId = leftOpponent.id,
|
||||||
|
rightOpponentId = rightOpponent.id,
|
||||||
|
leagueAndSerieName = leagueAndSerieName,
|
||||||
|
scheduleString = scheduleConfig.first,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
MatchupRow(match.opponents)
|
MatchupRow(leftOpponent, rightOpponent)
|
||||||
LeagueInfoRow(match.league, match.serie.name)
|
LeagueInfoRow(match.league, leagueAndSerieName)
|
||||||
}
|
}
|
||||||
ScheduleBadge(match.status, match.beginAt)
|
ScheduleBadge(scheduleConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MatchupRow(opponents: List<Opponent>) {
|
private fun MatchupRow(leftOpponent: Opponent, rightOpponent: Opponent) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -66,7 +79,6 @@ private fun MatchupRow(opponents: List<Opponent>) {
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val (leftOpponent, rightOpponent) = getOrDefaultOpponents(opponents)
|
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
|
@ -110,7 +122,7 @@ private fun MatchupRow(opponents: List<Opponent>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeagueInfoRow(league: League, serieName: String) {
|
private fun LeagueInfoRow(league: League, leagueAndSerieName: String) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -123,15 +135,14 @@ private fun LeagueInfoRow(league: League, serieName: String) {
|
||||||
contentDescription = "${league.name} logo",
|
contentDescription = "${league.name} logo",
|
||||||
error = painterResource(R.drawable.fallback_image_round),
|
error = painterResource(R.drawable.fallback_image_round),
|
||||||
placeholder = painterResource(R.drawable.fallback_image_round),
|
placeholder = painterResource(R.drawable.fallback_image_round),
|
||||||
onLoading = {
|
onLoading = {},
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(16.dp)
|
.height(16.dp)
|
||||||
.wrapContentWidth(),
|
.wrapContentWidth(),
|
||||||
contentScale = ContentScale.Fit
|
contentScale = ContentScale.Fit
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"${league.name} + $serieName",
|
leagueAndSerieName,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
modifier = Modifier.padding(start = 8.dp)
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
)
|
)
|
||||||
|
@ -139,52 +150,20 @@ private fun LeagueInfoRow(league: League, serieName: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.ScheduleBadge(status: MatchStatus, beginAt: ZonedDateTime?) {
|
private fun BoxScope.ScheduleBadge(scheduleConfig: Pair<String, Color>) {
|
||||||
when (status) {
|
val (scheduleString, scheduleBackgroundColor) = scheduleConfig
|
||||||
MatchStatus.LIVE ->
|
Box(
|
||||||
Box(
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.align(Alignment.TopEnd)
|
||||||
.align(Alignment.TopEnd)
|
.clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp))
|
||||||
.clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp))
|
.background(scheduleBackgroundColor)
|
||||||
.background(LiveRed)
|
.padding(8.dp)
|
||||||
.padding(8.dp)
|
) {
|
||||||
) {
|
Text(
|
||||||
Text(
|
text = scheduleString,
|
||||||
text = "AGORA",
|
style = MaterialTheme.typography.displayMedium,
|
||||||
style = MaterialTheme.typography.displayMedium,
|
color = Color.White
|
||||||
color = Color.White
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MatchStatus.SCHEDULED ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp))
|
|
||||||
.background(White_20)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = beginAt.toFormattedMatchTime(),
|
|
||||||
style = MaterialTheme.typography.displayMedium,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MatchStatus.UNKNOWN ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.clip(RoundedCornerShape(topEnd = 16.dp, bottomStart = 16.dp))
|
|
||||||
.background(White_20)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "A ser definido",
|
|
||||||
style = MaterialTheme.typography.displayMedium,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,8 +176,7 @@ private fun Modifier.topBorder(color: Color, thickness: Dp): Modifier = this.the
|
||||||
end = Offset(size.width, 0f),
|
end = Offset(size.width, 0f),
|
||||||
strokeWidth = strokeWidth
|
strokeWidth = strokeWidth
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
private fun getOrDefaultOpponents(opponents: List<Opponent>): Pair<Opponent, Opponent> {
|
private fun getOrDefaultOpponents(opponents: List<Opponent>): Pair<Opponent, Opponent> {
|
||||||
val default = Opponent(id = 0, name = "A ser definido", imageUrl = "")
|
val default = Opponent(id = 0, name = "A ser definido", imageUrl = "")
|
||||||
|
@ -209,3 +187,8 @@ private fun getOrDefaultOpponents(opponents: List<Opponent>): Pair<Opponent, Opp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MatchStatus.scheduleConfigFor(beginAt: ZonedDateTime?) = when (this) {
|
||||||
|
MatchStatus.LIVE -> "Agora" to LiveRed
|
||||||
|
MatchStatus.SCHEDULED -> beginAt.toFormattedMatchTime() to White_20
|
||||||
|
MatchStatus.UNKNOWN -> "A definir" to White_20
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,21 @@ package xyz.leomurca.csgomatches.ui.navigation
|
||||||
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import xyz.leomurca.csgomatches.ui.screens.details.DetailsScreen
|
import xyz.leomurca.csgomatches.ui.screens.details.DetailsScreen
|
||||||
|
|
||||||
const val DETAILS_ROUTE = "details"
|
@Serializable
|
||||||
|
data class DetailsRoute(
|
||||||
|
val leftOpponentId: Long,
|
||||||
|
val rightOpponentId: Long,
|
||||||
|
val leagueAndSerieName: String,
|
||||||
|
val scheduleString: String
|
||||||
|
)
|
||||||
|
|
||||||
fun NavGraphBuilder.detailsScreen() {
|
fun NavGraphBuilder.detailsScreen(onBackClick: () -> Unit) {
|
||||||
composable(DETAILS_ROUTE) {
|
composable<DetailsRoute> {
|
||||||
DetailsScreen()
|
val details: DetailsRoute = it.toRoute()
|
||||||
|
DetailsScreen(details = details, onBackClick = onBackClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,16 @@ package xyz.leomurca.csgomatches.ui.navigation
|
||||||
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import xyz.leomurca.csgomatches.domain.model.Match
|
||||||
import xyz.leomurca.csgomatches.ui.screens.matches.MatchesScreen
|
import xyz.leomurca.csgomatches.ui.screens.matches.MatchesScreen
|
||||||
|
|
||||||
const val MATCHES_ROUTE = "matches"
|
@Serializable
|
||||||
|
object MatchesRoute
|
||||||
|
|
||||||
fun NavGraphBuilder.matchesScreen() {
|
fun NavGraphBuilder.matchesScreen(onTapCard: (DetailsRoute) -> Unit) {
|
||||||
composable(MATCHES_ROUTE) {
|
composable<MatchesRoute> {
|
||||||
MatchesScreen()
|
MatchesScreen(onTapCard = onTapCard)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,9 +9,13 @@ fun RootNavHost() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = MATCHES_ROUTE
|
startDestination = MatchesRoute
|
||||||
) {
|
) {
|
||||||
matchesScreen()
|
matchesScreen {
|
||||||
detailsScreen()
|
navController.navigate(it)
|
||||||
|
}
|
||||||
|
detailsScreen {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,20 +2,84 @@ package xyz.leomurca.csgomatches.ui.screens.details
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import xyz.leomurca.csgomatches.ui.components.LoadingIndicator
|
||||||
|
import xyz.leomurca.csgomatches.ui.navigation.DetailsRoute
|
||||||
|
import xyz.leomurca.csgomatches.ui.theme.White
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailsScreen() {
|
fun DetailsScreen(
|
||||||
|
viewModel: DetailsViewModel = hiltViewModel(),
|
||||||
|
details: DetailsRoute,
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val uiState = viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadTeam(details.leftOpponentId.toString(), isLeft = true)
|
||||||
|
viewModel.loadTeam(details.rightOpponentId.toString(), isLeft = false)
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text("Details Screen", fontSize = 24.sp)
|
TopBar(details.leagueAndSerieName, onBackClick)
|
||||||
|
|
||||||
|
val value = uiState.value
|
||||||
|
when {
|
||||||
|
value.errorMessage != null -> Text(value.errorMessage)
|
||||||
|
value.isLoading -> LoadingIndicator()
|
||||||
|
value.leftTeam != null && value.rightTeam != null -> Column {
|
||||||
|
Text(value.leftTeam.name.toString())
|
||||||
|
Text(value.rightTeam.name.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.TopBar(leagueAndSerieName: String, onBackClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(top = 52.dp, start = 24.dp, end = 24.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
IconButton(onBackClick, modifier = Modifier) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "", tint = White)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
leagueAndSerieName,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = White,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package xyz.leomurca.csgomatches.ui.screens.details
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
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.Resource
|
||||||
|
import xyz.leomurca.csgomatches.domain.model.Team
|
||||||
|
import xyz.leomurca.csgomatches.domain.repository.MatchRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DetailsViewModel @Inject constructor(
|
||||||
|
private val matchRepository: MatchRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(TeamUiState())
|
||||||
|
val uiState: StateFlow<TeamUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun loadTeam(teamId: String, isLeft: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Error -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(errorMessage = result.message, isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TeamUiState(
|
||||||
|
val leftTeam: Team? = null,
|
||||||
|
val rightTeam: Team? = null,
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -24,12 +24,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import xyz.leomurca.csgomatches.domain.model.Match
|
import xyz.leomurca.csgomatches.domain.model.Match
|
||||||
import xyz.leomurca.csgomatches.ui.components.LoadingIndicator
|
import xyz.leomurca.csgomatches.ui.components.LoadingIndicator
|
||||||
import xyz.leomurca.csgomatches.ui.components.MatchCard
|
import xyz.leomurca.csgomatches.ui.components.MatchCard
|
||||||
|
import xyz.leomurca.csgomatches.ui.navigation.DetailsRoute
|
||||||
import xyz.leomurca.csgomatches.ui.screens.matches.MatchesViewModel.MatchesUiState
|
import xyz.leomurca.csgomatches.ui.screens.matches.MatchesViewModel.MatchesUiState
|
||||||
import xyz.leomurca.csgomatches.ui.theme.White
|
import xyz.leomurca.csgomatches.ui.theme.White
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MatchesScreen(viewModel: MatchesViewModel = hiltViewModel()) {
|
fun MatchesScreen(
|
||||||
|
viewModel: MatchesViewModel = hiltViewModel(),
|
||||||
|
onTapCard: (DetailsRoute) -> Unit
|
||||||
|
) {
|
||||||
val uiState = viewModel.uiState.collectAsState()
|
val uiState = viewModel.uiState.collectAsState()
|
||||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ fun MatchesScreen(viewModel: MatchesViewModel = hiltViewModel()) {
|
||||||
"Partidas",
|
"Partidas",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = White,
|
color = White,
|
||||||
modifier = Modifier.padding(top = 24.dp, start = 24.dp)
|
modifier = Modifier.padding(top = 24.dp, start = 6.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
@ -60,7 +64,7 @@ fun MatchesScreen(viewModel: MatchesViewModel = hiltViewModel()) {
|
||||||
) {
|
) {
|
||||||
when (val value = uiState.value) {
|
when (val value = uiState.value) {
|
||||||
is MatchesUiState.Loading -> LoadingIndicator()
|
is MatchesUiState.Loading -> LoadingIndicator()
|
||||||
is MatchesUiState.Success -> MatchesList(value.matches)
|
is MatchesUiState.Success -> MatchesList(value.matches, onTapCard)
|
||||||
is MatchesUiState.Error -> Text(value.message)
|
is MatchesUiState.Error -> Text(value.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +72,7 @@ fun MatchesScreen(viewModel: MatchesViewModel = hiltViewModel()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MatchesList(matches: List<Match>) {
|
private fun MatchesList(matches: List<Match>, onTapCard: (DetailsRoute) -> Unit) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
|
@ -76,7 +80,7 @@ private fun MatchesList(matches: List<Match>) {
|
||||||
contentPadding = PaddingValues(vertical = 24.dp)
|
contentPadding = PaddingValues(vertical = 24.dp)
|
||||||
) {
|
) {
|
||||||
items(matches) {
|
items(matches) {
|
||||||
MatchCard(it)
|
MatchCard(it, onTapCard)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue