Filters on bets screen
continuous-integration/drone/push Build is passing Details

pull/5/head
avalin 11 months ago
parent 4ce266860f
commit 4612f88c69

@ -0,0 +1,18 @@
package fr.iut.alldev.allin.ext
import androidx.annotation.StringRes
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.bet.BetFilter
import fr.iut.alldev.allin.data.model.bet.BetFilter.FINISHED
import fr.iut.alldev.allin.data.model.bet.BetFilter.INVITATION
import fr.iut.alldev.allin.data.model.bet.BetFilter.IN_PROGRESS
import fr.iut.alldev.allin.data.model.bet.BetFilter.PUBLIC
@StringRes
fun BetFilter.textId() =
when (this) {
PUBLIC -> R.string.Public
INVITATION -> R.string.Invitation
IN_PROGRESS -> R.string.Current
FINISHED -> R.string.Finished
}

@ -23,10 +23,9 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.graphics.Brush import androidx.compose.ui.graphics.Brush
@ -35,10 +34,11 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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 fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.ext.formatToMediumDateNoYear import fr.iut.alldev.allin.data.ext.formatToMediumDateNoYear
import fr.iut.alldev.allin.data.ext.formatToTime import fr.iut.alldev.allin.data.ext.formatToTime
import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetFilter
import fr.iut.alldev.allin.ext.textId
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.bet.components.BetScreenCard import fr.iut.alldev.allin.ui.bet.components.BetScreenCard
import fr.iut.alldev.allin.ui.bet.components.BetScreenPopularCard import fr.iut.alldev.allin.ui.bet.components.BetScreenPopularCard
@ -51,6 +51,7 @@ fun BetScreen(
selectBet: (Bet, Boolean) -> Unit, selectBet: (Bet, Boolean) -> Unit,
) { ) {
val bets by viewModel.bets.collectAsState() val bets by viewModel.bets.collectAsState()
val filters by viewModel.filters.collectAsState()
val refreshing by viewModel.isRefreshing.collectAsState() val refreshing by viewModel.isRefreshing.collectAsState()
val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.refresh() }) val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.refresh() })
@ -62,7 +63,7 @@ fun BetScreen(
.padding(top = with(LocalDensity.current) { .padding(top = with(LocalDensity.current) {
progressAnimation.toDp() progressAnimation.toDp()
}), }),
contentPadding = WindowInsets.navigationBars.asPaddingValues() contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) { ) {
item { item {
Box( Box(
@ -98,18 +99,24 @@ fun BetScreen(
horizontalArrangement = Arrangement.spacedBy(9.dp), horizontalArrangement = Arrangement.spacedBy(9.dp),
contentPadding = PaddingValues(horizontal = 23.dp) contentPadding = PaddingValues(horizontal = 23.dp)
) { ) {
items(items) { items(BetFilter.entries) {
var isSelected by remember { mutableStateOf(false) } val isSelected by remember {
derivedStateOf {
filters.contains(it)
}
}
AllInChip( AllInChip(
text = stringResource(id = it), text = stringResource(id = it.textId()),
isSelected = isSelected, isSelected = isSelected,
onClick = { onClick = { viewModel.toggleFilter(it) }
isSelected = !isSelected )
})
} }
} }
} }
itemsIndexed(bets) { idx, it -> itemsIndexed(
items = bets,
key = { _, it -> it.id }
) { idx, it ->
BetScreenCard( BetScreenCard(
creator = it.creator, creator = it.creator,
category = it.theme, category = it.theme,
@ -119,7 +126,9 @@ fun BetScreen(
players = List(3) { null }, players = List(3) { null },
onClickParticipate = { selectBet(it, true) }, onClickParticipate = { selectBet(it, true) },
onClickCard = { selectBet(it, false) }, onClickCard = { selectBet(it, false) },
modifier = Modifier.padding(horizontal = 23.dp) modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 23.dp)
) )
if (idx != bets.lastIndex) { if (idx != bets.lastIndex) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@ -127,10 +136,3 @@ fun BetScreen(
} }
} }
} }
val items = listOf(
R.string.Public,
R.string.Invitation,
R.string.Current,
R.string.Finished
)

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetFilter
import fr.iut.alldev.allin.data.repository.BetRepository import fr.iut.alldev.allin.data.repository.BetRepository
import fr.iut.alldev.allin.keystore.AllInKeystoreManager import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -39,22 +41,48 @@ class BetViewModel @Inject constructor(
) )
} }
private val _filters: MutableStateFlow<List<BetFilter>> by lazy {
MutableStateFlow(BetFilter.entries)
}
val filters by lazy { _filters.asStateFlow() }
init { init {
viewModelScope.launch { viewModelScope.launch {
refreshData() refreshBets()
filters.collect { refreshBets() }
} }
} }
private suspend fun refreshData() { fun toggleFilter(filter: BetFilter) {
runCatching { viewModelScope.launch {
_bets.emit(betRepository.getAllBets(keystoreManager.getTokenOrEmpty())) _filters.emit(
if (_filters.value.contains(filter)) {
_filters.value - filter
} else {
_filters.value + filter
}
)
}
}
private suspend fun refreshBets() {
try {
_bets.emit(
betRepository.getAllBets(
token = keystoreManager.getTokenOrEmpty(),
filters = filters.value
)
)
} catch (e: Exception) {
Timber.e(e)
} }
} }
fun refresh() { fun refresh() {
viewModelScope.launch { viewModelScope.launch {
_isRefreshing.emit(true) _isRefreshing.emit(true)
refreshData() refreshBets()
_isRefreshing.emit(false) _isRefreshing.emit(false)
} }
} }

@ -178,7 +178,7 @@ private fun BetStatusParticipationBottomSheetContent(
color = AllInTheme.colors.onBackground color = AllInTheme.colors.onBackground
) )
AllInCoinCount( AllInCoinCount(
amount = stake?.let { (it + (it * odds)).roundToInt() } ?: 0, amount = stake?.let { (it * odds).roundToInt() } ?: 0,
color = AllInTheme.colors.onBackground color = AllInTheme.colors.onBackground
) )
} }

@ -33,7 +33,10 @@ class LoginViewModel @Inject constructor(
userRepository userRepository
.login(username.value, password.value) .login(username.value, password.value)
?.let { token -> keystoreManager.putToken(token) } ?.let { token -> keystoreManager.putToken(token) }
navigateToDashboard()
withContext(Dispatchers.Main) {
navigateToDashboard()
}
} catch (e: Exception) { } catch (e: Exception) {
hasError.value = true hasError.value = true

@ -106,6 +106,10 @@ class RegisterViewModel @Inject constructor(
password.value password.value
) )
?.let { token -> keystoreManager.putToken(token) } ?.let { token -> keystoreManager.putToken(token) }
withContext(Dispatchers.Main) {
navigateToDashboard()
}
} catch (e: Exception) { } catch (e: Exception) {
when { when {
e is HttpException && e.code() == 409 -> { e is HttpException && e.code() == 409 -> {
@ -122,9 +126,6 @@ class RegisterViewModel @Inject constructor(
} }
} }
} }
if (!hasError.value) {
navigateToDashboard()
}
loading.value = false loading.value = false
} }
} }

@ -2,6 +2,7 @@ package fr.iut.alldev.allin.data.api
import fr.iut.alldev.allin.data.api.model.CheckUser import fr.iut.alldev.allin.data.api.model.CheckUser
import fr.iut.alldev.allin.data.api.model.RequestBet import fr.iut.alldev.allin.data.api.model.RequestBet
import fr.iut.alldev.allin.data.api.model.RequestBetFilters
import fr.iut.alldev.allin.data.api.model.RequestParticipation import fr.iut.alldev.allin.data.api.model.RequestParticipation
import fr.iut.alldev.allin.data.api.model.RequestUser import fr.iut.alldev.allin.data.api.model.RequestUser
import fr.iut.alldev.allin.data.api.model.ResponseBet import fr.iut.alldev.allin.data.api.model.ResponseBet
@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.RequestBody
import java.time.Duration import java.time.Duration
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
@ -133,7 +135,7 @@ class MockAllInApi : AllInApi {
} else throw MockAllInApiException("Gift already taken today") } else throw MockAllInApiException("Gift already taken today")
} }
override suspend fun getAllBets(token: String): List<ResponseBet> { override suspend fun getAllBets(token: String, body: RequestBetFilters): List<ResponseBet> {
getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.") getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
return mockBets return mockBets
} }
@ -155,13 +157,13 @@ class MockAllInApi : AllInApi {
} }
} }
override suspend fun confirmBet(token: String, id: String, value: String) { override suspend fun confirmBet(token: String, id: String, value: RequestBody) {
getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.") getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
val bet = mockBets.find { it.id == id } ?: throw MockAllInApiException("Unauthorized") val bet = mockBets.find { it.id == id } ?: throw MockAllInApiException("Unauthorized")
mockResults.add( mockResults.add(
ResponseBetResult( ResponseBetResult(
betId = id, betId = id,
result = value result = value.toString()
) )
) )
mockBets[mockBets.indexOf(bet)] = bet.copy(status = BetStatus.FINISHED) mockBets[mockBets.indexOf(bet)] = bet.copy(status = BetStatus.FINISHED)

@ -2,12 +2,16 @@ package fr.iut.alldev.allin.data.api
import fr.iut.alldev.allin.data.api.model.CheckUser import fr.iut.alldev.allin.data.api.model.CheckUser
import fr.iut.alldev.allin.data.api.model.RequestBet import fr.iut.alldev.allin.data.api.model.RequestBet
import fr.iut.alldev.allin.data.api.model.RequestBetFilters
import fr.iut.alldev.allin.data.api.model.RequestParticipation import fr.iut.alldev.allin.data.api.model.RequestParticipation
import fr.iut.alldev.allin.data.api.model.RequestUser import fr.iut.alldev.allin.data.api.model.RequestUser
import fr.iut.alldev.allin.data.api.model.ResponseBet import fr.iut.alldev.allin.data.api.model.ResponseBet
import fr.iut.alldev.allin.data.api.model.ResponseBetDetail import fr.iut.alldev.allin.data.api.model.ResponseBetDetail
import fr.iut.alldev.allin.data.api.model.ResponseBetResultDetail import fr.iut.alldev.allin.data.api.model.ResponseBetResultDetail
import fr.iut.alldev.allin.data.api.model.ResponseUser import fr.iut.alldev.allin.data.api.model.ResponseUser
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
@ -17,6 +21,9 @@ import retrofit2.http.Path
interface AllInApi { interface AllInApi {
companion object { companion object {
fun String.formatBearerToken() = "Bearer $this" fun String.formatBearerToken() = "Bearer $this"
fun String.asRequestBody() =
this.toRequestBody("text/plain".toMediaTypeOrNull())
} }
@POST("users/login") @POST("users/login")
@ -34,8 +41,11 @@ interface AllInApi {
@GET("users/gift") @GET("users/gift")
suspend fun dailyGift(@Header("Authorization") token: String): Int suspend fun dailyGift(@Header("Authorization") token: String): Int
@GET("bets/gets") @POST("bets/gets")
suspend fun getAllBets(@Header("Authorization") token: String): List<ResponseBet> suspend fun getAllBets(
@Header("Authorization") token: String,
@Body body: RequestBetFilters
): List<ResponseBet>
@GET("bets/toConfirm") @GET("bets/toConfirm")
suspend fun getToConfirm(@Header("Authorization") token: String): List<ResponseBetDetail> suspend fun getToConfirm(@Header("Authorization") token: String): List<ResponseBetDetail>
@ -44,7 +54,7 @@ interface AllInApi {
suspend fun confirmBet( suspend fun confirmBet(
@Header("Authorization") token: String, @Header("Authorization") token: String,
@Path("id") id: String, @Path("id") id: String,
@Body value: String @Body value: RequestBody
) )
@GET("betdetail/get/{id}") @GET("betdetail/get/{id}")
@ -53,7 +63,7 @@ interface AllInApi {
@Path("id") id: String @Path("id") id: String
): ResponseBetDetail ): ResponseBetDetail
@GET("bets/getCurrent") @GET("bets/current")
suspend fun getBetCurrent( suspend fun getBetCurrent(
@Header("Authorization") token: String @Header("Authorization") token: String
): List<ResponseBetDetail> ): List<ResponseBetDetail>

@ -2,6 +2,7 @@ package fr.iut.alldev.allin.data.api.model
import androidx.annotation.Keep import androidx.annotation.Keep
import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetFilter
import fr.iut.alldev.allin.data.model.bet.BetResult import fr.iut.alldev.allin.data.model.bet.BetResult
import fr.iut.alldev.allin.data.model.bet.BetResultDetail import fr.iut.alldev.allin.data.model.bet.BetResultDetail
import fr.iut.alldev.allin.data.model.bet.BetStatus import fr.iut.alldev.allin.data.model.bet.BetStatus
@ -157,3 +158,8 @@ data class ResponseBetResultDetail(
won = won won = won
) )
} }
@Serializable
data class RequestBetFilters(
val filters: List<BetFilter>
)

@ -0,0 +1,8 @@
package fr.iut.alldev.allin.data.model.bet
enum class BetFilter {
PUBLIC,
INVITATION,
IN_PROGRESS,
FINISHED
}

@ -1,6 +1,7 @@
package fr.iut.alldev.allin.data.repository package fr.iut.alldev.allin.data.repository
import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetFilter
import fr.iut.alldev.allin.data.model.bet.BetResultDetail import fr.iut.alldev.allin.data.model.bet.BetResultDetail
import fr.iut.alldev.allin.data.model.bet.Participation import fr.iut.alldev.allin.data.model.bet.Participation
import fr.iut.alldev.allin.data.model.bet.vo.BetDetail import fr.iut.alldev.allin.data.model.bet.vo.BetDetail
@ -11,7 +12,7 @@ abstract class BetRepository {
abstract suspend fun getCurrentBets(token: String): List<BetDetail> abstract suspend fun getCurrentBets(token: String): List<BetDetail>
abstract suspend fun getBet(id: String, token: String): BetDetail abstract suspend fun getBet(id: String, token: String): BetDetail
abstract suspend fun participateToBet(participation: Participation, token: String) abstract suspend fun participateToBet(participation: Participation, token: String)
abstract suspend fun getAllBets(token: String): List<Bet> abstract suspend fun getAllBets(token: String, filters: List<BetFilter>): List<Bet>
abstract suspend fun confirmBet(token: String, id: String, response: String) abstract suspend fun confirmBet(token: String, id: String, response: String)
abstract suspend fun getToConfirm(token: String): List<BetDetail> abstract suspend fun getToConfirm(token: String): List<BetDetail>
abstract suspend fun getWon(token: String): List<BetResultDetail> abstract suspend fun getWon(token: String): List<BetResultDetail>

@ -1,8 +1,11 @@
package fr.iut.alldev.allin.data.repository.impl package fr.iut.alldev.allin.data.repository.impl
import fr.iut.alldev.allin.data.api.AllInApi import fr.iut.alldev.allin.data.api.AllInApi
import fr.iut.alldev.allin.data.api.AllInApi.Companion.asRequestBody
import fr.iut.alldev.allin.data.api.AllInApi.Companion.formatBearerToken import fr.iut.alldev.allin.data.api.AllInApi.Companion.formatBearerToken
import fr.iut.alldev.allin.data.api.model.RequestBetFilters
import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetFilter
import fr.iut.alldev.allin.data.model.bet.BetResultDetail import fr.iut.alldev.allin.data.model.bet.BetResultDetail
import fr.iut.alldev.allin.data.model.bet.Participation import fr.iut.alldev.allin.data.model.bet.Participation
import fr.iut.alldev.allin.data.model.bet.vo.BetDetail import fr.iut.alldev.allin.data.model.bet.vo.BetDetail
@ -45,8 +48,11 @@ class BetRepositoryImpl @Inject constructor(
) )
} }
override suspend fun getAllBets(token: String): List<Bet> = override suspend fun getAllBets(token: String, filters: List<BetFilter>): List<Bet> =
api.getAllBets(token.formatBearerToken()).map { it.toBet() } api.getAllBets(
token.formatBearerToken(),
RequestBetFilters(filters)
).map { it.toBet() }
override suspend fun getToConfirm(token: String): List<BetDetail> = override suspend fun getToConfirm(token: String): List<BetDetail> =
api.getToConfirm(token.formatBearerToken()).map { it.toBetDetail() } api.getToConfirm(token.formatBearerToken()).map { it.toBetDetail() }
@ -55,7 +61,7 @@ class BetRepositoryImpl @Inject constructor(
api.getWon(token.formatBearerToken()).map { it.toBetResultDetail() } api.getWon(token.formatBearerToken()).map { it.toBetResultDetail() }
override suspend fun confirmBet(token: String, id: String, response: String) { override suspend fun confirmBet(token: String, id: String, response: String) {
api.confirmBet(token.formatBearerToken(), id, response) api.confirmBet(token.formatBearerToken(), id, response.asRequestBody())
} }
} }

@ -12,7 +12,7 @@ androidxCore = "1.12.0"
androidxActivity = "1.8.2" androidxActivity = "1.8.2"
androidxSecurity = "1.1.0-alpha06" androidxSecurity = "1.1.0-alpha06"
composeBom = "2024.04.00" composeBom = "2024.05.00"
composeCompiler = "1.5.5" composeCompiler = "1.5.5"
composePreview = "1.6.5" composePreview = "1.6.5"
composeNavigation = "2.7.6" composeNavigation = "2.7.6"

Loading…
Cancel
Save