Custom bet creation
continuous-integration/drone/push Build is passing Details

pull/5/head
avalin 11 months ago
parent 38a42582f3
commit 0005994ea7

@ -37,6 +37,8 @@ sealed class FieldErrorState(
data class DateOrder(val fieldName1: String, val fieldName2: String) : data class DateOrder(val fieldName1: String, val fieldName2: String) :
FieldErrorState(R.string.field_error_date_order, fieldName1, fieldName2) FieldErrorState(R.string.field_error_date_order, fieldName1, fieldName2)
data object NoResponse :
FieldErrorState(R.string.field_error_no_response)
@Composable @Composable
fun errorResource() = stringResourceOrNull(id = messageId, *messageArgs) fun errorResource() = stringResourceOrNull(id = messageId, *messageArgs)

@ -1,17 +1,26 @@
package fr.iut.alldev.allin.ext package fr.iut.alldev.allin.ext
import android.graphics.BlurMaskFilter import android.graphics.BlurMaskFilter
import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.LinearGradientShader import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.min
@Composable @Composable
fun Modifier.shadow( fun Modifier.shadow(
@ -89,3 +98,49 @@ fun Modifier.shadow(
} }
} }
) )
fun Modifier.nonLinkedScroll() =
this.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
) = available.copy(x = 0f)
})
fun Modifier.fadingEdges(
scrollState: ScrollState,
topEdgeHeight: Dp = 16.dp,
bottomEdgeHeight: Dp = 16.dp
): Modifier = this.then(
Modifier
// adding layer fixes issue with blending gradient and content
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
drawContent()
val topColors = listOf(Color.Transparent, Color.Black)
val topStartY = scrollState.value.toFloat()
val topGradientHeight = min(topEdgeHeight.toPx(), topStartY)
drawRect(
brush = Brush.verticalGradient(
colors = topColors,
startY = topStartY,
endY = topStartY + topGradientHeight
),
blendMode = BlendMode.DstIn
)
val bottomColors = listOf(Color.Black, Color.Transparent)
val bottomEndY = size.height - scrollState.maxValue + scrollState.value
val bottomGradientHeight = min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value)
if (bottomGradientHeight != 0f) drawRect(
brush = Brush.verticalGradient(
colors = bottomColors,
startY = bottomEndY - bottomGradientHeight,
endY = bottomEndY
),
blendMode = BlendMode.DstIn
)
}
)

@ -47,6 +47,13 @@ fun WindowInsets.takeBottomOnly(): WindowInsets {
return WindowInsets(bottom = this.getBottom(density)) return WindowInsets(bottom = this.getBottom(density))
} }
@ReadOnlyComposable
@Composable
fun WindowInsets.takeTopOnly(): WindowInsets {
val density = LocalDensity.current
return WindowInsets(top = this.getTop(density))
}
@Composable @Composable
fun bottomSheetNavigationBarsInsets(): WindowInsets { fun bottomSheetNavigationBarsInsets(): WindowInsets {
val density = LocalDensity.current val density = LocalDensity.current

@ -7,15 +7,17 @@ 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.Spacer
import androidx.compose.foundation.layout.WindowInsets
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.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -51,7 +53,12 @@ import fr.iut.alldev.allin.data.model.bet.NO_VALUE
import fr.iut.alldev.allin.data.model.bet.YES_VALUE import fr.iut.alldev.allin.data.model.bet.YES_VALUE
import fr.iut.alldev.allin.data.model.bet.vo.BetAnswerDetail import fr.iut.alldev.allin.data.model.bet.vo.BetAnswerDetail
import fr.iut.alldev.allin.data.model.bet.vo.BetDetail import fr.iut.alldev.allin.data.model.bet.vo.BetDetail
import fr.iut.alldev.allin.ext.asPaddingValues
import fr.iut.alldev.allin.ext.bottomSheetNavigationBarsInsets
import fr.iut.alldev.allin.ext.fadingEdges
import fr.iut.alldev.allin.ext.formatToSimple import fr.iut.alldev.allin.ext.formatToSimple
import fr.iut.alldev.allin.ext.nonLinkedScroll
import fr.iut.alldev.allin.ext.takeTopOnly
import fr.iut.alldev.allin.theme.AllInColorToken import fr.iut.alldev.allin.theme.AllInColorToken
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.core.AllInBottomSheet import fr.iut.alldev.allin.ui.core.AllInBottomSheet
@ -148,6 +155,7 @@ fun ConfirmationAnswers(
) { ) {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val locale = remember { ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() } val locale = remember { ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() }
val scrollState = rememberScrollState()
val possibleAnswers = remember { val possibleAnswers = remember {
when (val bet = betDetail.bet) { when (val bet = betDetail.bet) {
@ -157,10 +165,14 @@ fun ConfirmationAnswers(
} }
} }
LazyColumn( Column(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.nonLinkedScroll()
.verticalScroll(scrollState)
.fadingEdges(scrollState)
) { ) {
itemsIndexed(possibleAnswers) { idx, it -> possibleAnswers.forEachIndexed { idx, it ->
betDetail.getAnswerOfResponse(it)?.let { betDetail.getAnswerOfResponse(it)?.let {
val opacity by animateFloatAsState( val opacity by animateFloatAsState(
targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f, targetValue = if (selectedAnswer != null && selectedAnswer != it.response) .5f else 1f,
@ -182,6 +194,9 @@ fun ConfirmationAnswers(
) )
} }
} }
Spacer(
modifier = Modifier.padding(bottomSheetNavigationBarsInsets().asPaddingValues(bottom = 56.dp))
)
} }
} }
@ -197,7 +212,11 @@ fun BetConfirmationBottomSheetContent(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.safeContentPadding() .padding(
WindowInsets.safeContent
.takeTopOnly()
.asPaddingValues()
)
.padding(16.dp) .padding(16.dp)
) { ) {
IconButton( IconButton(
@ -283,6 +302,7 @@ fun BetConfirmationBottomSheetContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.safeContentPadding()
) )
} }
} }

@ -13,6 +13,7 @@ import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.bet.BetType import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.ext.getIcon import fr.iut.alldev.allin.ext.getIcon
import fr.iut.alldev.allin.ext.getTitleId import fr.iut.alldev.allin.ext.getTitleId
import fr.iut.alldev.allin.ui.betCreation.BetCreationViewModel.BetTypeState.Companion.typeState
import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenContent import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenContent
import fr.iut.alldev.allin.ui.core.AllInAlertDialog import fr.iut.alldev.allin.ui.core.AllInAlertDialog
import fr.iut.alldev.allin.ui.core.AllInDatePicker import fr.iut.alldev.allin.ui.core.AllInDatePicker
@ -42,6 +43,7 @@ fun BetCreationScreen(
val phraseError by remember { viewModel.phraseError } val phraseError by remember { viewModel.phraseError }
val registerDateError by remember { viewModel.registerDateError } val registerDateError by remember { viewModel.registerDateError }
val betDateError by remember { viewModel.betDateError } val betDateError by remember { viewModel.betDateError }
val typeError by remember { viewModel.typeError }
val friends by viewModel.friends.collectAsStateWithLifecycle() val friends by viewModel.friends.collectAsStateWithLifecycle()
@ -63,7 +65,7 @@ fun BetCreationScreen(
} }
LaunchedEffect(key1 = selectedBetTypeElement) { LaunchedEffect(key1 = selectedBetTypeElement) {
selectedBetType = betTypes[selectionElements.indexOf(selectedBetTypeElement)] selectedBetType = betTypes[selectionElements.indexOf(selectedBetTypeElement)].typeState()
} }
val (showRegisterDatePicker, setRegisterDatePicker) = remember { mutableStateOf(false) } val (showRegisterDatePicker, setRegisterDatePicker) = remember { mutableStateOf(false) }
@ -85,6 +87,7 @@ fun BetCreationScreen(
registerDateError = registerDateError.errorResource(), registerDateError = registerDateError.errorResource(),
betDate = betDate, betDate = betDate,
betDateError = betDateError.errorResource(), betDateError = betDateError.errorResource(),
typeError = typeError.errorResource(),
friends = friends, friends = friends,
selectedFriends = selectedFriends, selectedFriends = selectedFriends,
setRegisterDateDialog = setRegisterDatePicker, setRegisterDateDialog = setRegisterDatePicker,
@ -95,6 +98,8 @@ fun BetCreationScreen(
selectedBetType = selectedBetType, selectedBetType = selectedBetType,
setSelectedBetTypeElement = { selectedBetTypeElement = it }, setSelectedBetTypeElement = { selectedBetTypeElement = it },
selectionBetType = selectionElements, selectionBetType = selectionElements,
addAnswer = { viewModel.addAnswer(it) },
deleteAnswer = { viewModel.deleteAnswer(it) },
onCreateBet = { onCreateBet = {
viewModel.createBet( viewModel.createBet(
themeFieldName = themeFieldName, themeFieldName = themeFieldName,

@ -6,8 +6,11 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.model.FriendStatus import fr.iut.alldev.allin.data.model.FriendStatus
import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.data.model.User
import fr.iut.alldev.allin.data.model.bet.BetFactory import fr.iut.alldev.allin.data.model.bet.BetStatus
import fr.iut.alldev.allin.data.model.bet.BetType import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.data.model.bet.BinaryBet
import fr.iut.alldev.allin.data.model.bet.CustomBet
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.repository.BetRepository import fr.iut.alldev.allin.data.repository.BetRepository
import fr.iut.alldev.allin.data.repository.FriendRepository import fr.iut.alldev.allin.data.repository.FriendRepository
import fr.iut.alldev.allin.data.repository.UserRepository import fr.iut.alldev.allin.data.repository.UserRepository
@ -34,12 +37,13 @@ class BetCreationViewModel @Inject constructor(
val registerDate = mutableStateOf(ZonedDateTime.now()) val registerDate = mutableStateOf(ZonedDateTime.now())
val betDate = mutableStateOf(ZonedDateTime.now()) val betDate = mutableStateOf(ZonedDateTime.now())
var isPublic = mutableStateOf(true) var isPublic = mutableStateOf(true)
var selectedBetType = mutableStateOf(BetType.BINARY) var selectedBetType = mutableStateOf<BetTypeState>(BetTypeState.Binary)
val themeError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError) val themeError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val phraseError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError) val phraseError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val registerDateError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError) val registerDateError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val betDateError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError) val betDateError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
val typeError = mutableStateOf<FieldErrorState>(FieldErrorState.NoError)
private val _friends by lazy { MutableStateFlow<List<User>>(emptyList()) } private val _friends by lazy { MutableStateFlow<List<User>>(emptyList()) }
val friends get() = _friends.asStateFlow() val friends get() = _friends.asStateFlow()
@ -59,6 +63,7 @@ class BetCreationViewModel @Inject constructor(
phraseError.value = FieldErrorState.NoError phraseError.value = FieldErrorState.NoError
registerDateError.value = FieldErrorState.NoError registerDateError.value = FieldErrorState.NoError
betDateError.value = FieldErrorState.NoError betDateError.value = FieldErrorState.NoError
typeError.value = FieldErrorState.NoError
hasError.value = false hasError.value = false
} }
@ -94,6 +99,19 @@ class BetCreationViewModel @Inject constructor(
) )
hasError.value = true hasError.value = true
} }
when (val state = selectedBetType.value) {
BetTypeState.Binary -> Unit
is BetTypeState.Custom -> if (state.possibleAnswers.size < 2) {
typeError.value = FieldErrorState.NoResponse
hasError.value = true
}
is BetTypeState.Match -> if (state.team1.isBlank() || state.team2.isBlank()){
typeError.value = FieldErrorState.Mandatory
hasError.value = true
}
}
} }
@ -116,19 +134,58 @@ class BetCreationViewModel @Inject constructor(
if (!hasError.value) { if (!hasError.value) {
try { try {
userRepository.currentUserState.value?.let { currentUser -> userRepository.currentUserState.value?.let { currentUser ->
val bet = BetFactory.createBet( val bet =
when (val type = selectedBetType.value) {
BetTypeState.Binary -> {
BinaryBet(
id = "",
theme = theme.value,
creator = currentUser.username,
phrase = phrase.value,
endRegisterDate = registerDate.value,
endBetDate = betDate.value,
isPublic = isPublic.value,
betStatus = BetStatus.WAITING,
totalStakes = 0,
totalParticipants = 0
)
}
is BetTypeState.Match -> {
MatchBet(
id = "", id = "",
betType = selectedBetType.value,
theme = theme.value, theme = theme.value,
creator = currentUser.username,
phrase = phrase.value, phrase = phrase.value,
endRegisterDate = registerDate.value, endRegisterDate = registerDate.value,
endBetDate = betDate.value, endBetDate = betDate.value,
isPublic = isPublic.value, isPublic = isPublic.value,
nameTeam1 = "", betStatus = BetStatus.WAITING,
nameTeam2 = "", totalStakes = 0,
possibleAnswers = listOf(), totalParticipants = 0,
creator = currentUser.username nameTeam1 = type.team1,
nameTeam2 = type.team2
) )
}
is BetTypeState.Custom -> {
CustomBet(
id = "",
theme = theme.value,
creator = currentUser.username,
phrase = phrase.value,
endRegisterDate = registerDate.value,
endBetDate = betDate.value,
isPublic = isPublic.value,
betStatus = BetStatus.WAITING,
totalStakes = 0,
totalParticipants = 0,
possibleAnswers = type.possibleAnswers
)
}
}
betRepository.createBet(bet, keystoreManager.getTokenOrEmpty()) betRepository.createBet(bet, keystoreManager.getTokenOrEmpty())
onSuccess() onSuccess()
} ?: onError() } ?: onError()
@ -140,4 +197,40 @@ class BetCreationViewModel @Inject constructor(
setLoading(false) setLoading(false)
} }
} }
fun addAnswer(value: String) {
viewModelScope.launch {
selectedBetType.value.let {
if (it is BetTypeState.Custom) {
selectedBetType.value = BetTypeState.Custom(possibleAnswers = it.possibleAnswers + value)
}
}
}
}
fun deleteAnswer(value: String) {
viewModelScope.launch {
selectedBetType.value.let {
if (it is BetTypeState.Custom) {
selectedBetType.value = BetTypeState.Custom(possibleAnswers = it.possibleAnswers - value)
}
}
}
}
sealed class BetTypeState(val type: BetType) {
data object Binary : BetTypeState(type = BetType.BINARY)
data class Match(val team1: String, val team2: String) : BetTypeState(type = BetType.MATCH)
data class Custom(val possibleAnswers: List<String>) : BetTypeState(type = BetType.CUSTOM)
companion object {
fun BetType.typeState() =
when (this) {
BetType.BINARY -> Binary
BetType.MATCH -> Match(team1 = "", team2 = "")
BetType.CUSTOM -> Custom(emptyList())
}
}
}
} }

@ -18,8 +18,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.R import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.data.model.User
import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.betCreation.BetCreationViewModel
import fr.iut.alldev.allin.ui.betCreation.tabs.BetCreationScreenAnswerTab import fr.iut.alldev.allin.ui.betCreation.tabs.BetCreationScreenAnswerTab
import fr.iut.alldev.allin.ui.betCreation.tabs.BetCreationScreenQuestionTab import fr.iut.alldev.allin.ui.betCreation.tabs.BetCreationScreenQuestionTab
import fr.iut.alldev.allin.ui.core.AllInSections import fr.iut.alldev.allin.ui.core.AllInSections
@ -49,9 +49,12 @@ fun BetCreationScreenContent(
setRegisterTimeDialog: (Boolean) -> Unit, setRegisterTimeDialog: (Boolean) -> Unit,
setEndTimeDialog: (Boolean) -> Unit, setEndTimeDialog: (Boolean) -> Unit,
selectedBetTypeElement: SelectionElement?, selectedBetTypeElement: SelectionElement?,
selectedBetType: BetType, selectedBetType: BetCreationViewModel.BetTypeState,
typeError: String?,
setSelectedBetTypeElement: (SelectionElement) -> Unit, setSelectedBetTypeElement: (SelectionElement) -> Unit,
selectionBetType: List<SelectionElement>, selectionBetType: List<SelectionElement>,
addAnswer: (String) -> Unit,
deleteAnswer: (String) -> Unit,
onCreateBet: () -> Unit onCreateBet: () -> Unit
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -90,7 +93,10 @@ fun BetCreationScreenContent(
selectedBetType = selectedBetType, selectedBetType = selectedBetType,
selected = selectedBetTypeElement, selected = selectedBetTypeElement,
setSelected = setSelectedBetTypeElement, setSelected = setSelectedBetTypeElement,
elements = selectionBetType elements = selectionBetType,
addAnswer = addAnswer,
deleteAnswer = deleteAnswer,
typeError = typeError
) )
} }
) )
@ -128,6 +134,7 @@ private fun BetCreationScreenContentPreview() {
betThemeError = null, betThemeError = null,
setBetTheme = { }, setBetTheme = { },
betPhrase = "Bryon", betPhrase = "Bryon",
typeError = null,
betPhraseError = null, betPhraseError = null,
setBetPhrase = { }, setBetPhrase = { },
isPublic = false, isPublic = false,
@ -142,10 +149,12 @@ private fun BetCreationScreenContentPreview() {
setRegisterTimeDialog = { }, setRegisterTimeDialog = { },
setEndTimeDialog = { }, setEndTimeDialog = { },
selectedBetTypeElement = null, selectedBetTypeElement = null,
selectedBetType = BetType.BINARY, selectedBetType = BetCreationViewModel.BetTypeState.Binary,
setSelectedBetTypeElement = { }, setSelectedBetTypeElement = { },
selectionBetType = listOf(), selectionBetType = listOf(),
onCreateBet = { } onCreateBet = { },
addAnswer = { },
deleteAnswer = { }
) )
} }
} }

@ -0,0 +1,72 @@
package fr.iut.alldev.allin.ui.betCreation.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.theme.AllInColorToken
import fr.iut.alldev.allin.theme.AllInTheme
import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
@Composable
fun BetCreationScreenCustomAnswer(
text: String,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
shape = AbsoluteSmoothCornerShape(15.dp, 100),
color = AllInColorToken.allInPurple,
contentColor = AllInColorToken.white
) {
Row(
modifier = Modifier
.padding(vertical = 4.dp)
.padding(start = 8.dp, end = 2.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = text,
textAlign = TextAlign.Center,
style = AllInTheme.typography.h1
)
IconButton(
onClick = onDelete,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
}
}
}
@Preview
@Composable
private fun BetCreationScreenCustomAnswerPreview() {
AllInTheme {
BetCreationScreenCustomAnswer(
text = "Text",
onDelete = {}
)
}
}

@ -0,0 +1,74 @@
package fr.iut.alldev.allin.ui.betCreation.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.theme.AllInColorToken
import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.core.AllInButton
import fr.iut.alldev.allin.ui.core.AllInTextField
import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
@Composable
fun BetCreationScreenCustomAnswerTextField(
value: String,
setValue: (String) -> Unit,
enabled: Boolean,
buttonEnabled: Boolean,
onAdd: () -> Unit,
modifier: Modifier = Modifier
) {
AllInTextField(
value = value,
enabled = enabled,
modifier = modifier,
onValueChange = setValue,
maxChar = 15,
trailingContent = {
AllInButton(
color = AllInColorToken.allInPurple,
enabled = enabled && buttonEnabled,
text = stringResource(id = R.string.generic_add),
textColor = AllInColorToken.white,
shape = AbsoluteSmoothCornerShape(
cornerRadiusTR = 10.dp,
cornerRadiusBR = 10.dp,
smoothnessAsPercentTR = 100,
smoothnessAsPercentBR = 100
),
onClick = onAdd
)
}
)
}
@Preview
@Composable
private fun BetCreationScreenCustomAnswerTextFieldPreview() {
AllInTheme {
BetCreationScreenCustomAnswerTextField(
onAdd = {},
enabled = true,
buttonEnabled = true,
value = "Test",
setValue = { }
)
}
}
@Preview
@Composable
private fun BetCreationScreenCustomAnswerDisabledTextFieldPreview() {
AllInTheme {
BetCreationScreenCustomAnswerTextField(
onAdd = {},
enabled = false,
buttonEnabled = false,
value = "Test",
setValue = { }
)
}
}

@ -2,35 +2,48 @@ package fr.iut.alldev.allin.ui.betCreation.tabs
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import fr.iut.alldev.allin.R import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.ext.getTitleId import fr.iut.alldev.allin.ext.getTitleId
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.betCreation.BetCreationViewModel
import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenBottomText import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenBottomText
import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenCustomAnswer
import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenCustomAnswerTextField
import fr.iut.alldev.allin.ui.core.AllInErrorLine
import fr.iut.alldev.allin.ui.core.AllInSelectionBox import fr.iut.alldev.allin.ui.core.AllInSelectionBox
import fr.iut.alldev.allin.ui.core.SelectionElement import fr.iut.alldev.allin.ui.core.SelectionElement
private const val BET_MAX_ANSWERS = 4
@Composable @Composable
fun BetCreationScreenAnswerTab( fun BetCreationScreenAnswerTab(
typeError: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selected: SelectionElement?, selected: SelectionElement?,
selectedBetType: BetType, selectedBetType: BetCreationViewModel.BetTypeState,
setSelected: (SelectionElement) -> Unit, setSelected: (SelectionElement) -> Unit,
elements: List<SelectionElement> elements: List<SelectionElement>,
addAnswer: (String) -> Unit,
deleteAnswer: (String) -> Unit
) { ) {
var isOpen by remember { var isOpen by remember {
mutableStateOf(false) mutableStateOf(false)
@ -50,31 +63,63 @@ fun BetCreationScreenAnswerTab(
elements = elements elements = elements
) )
Spacer(modifier = Modifier.height(26.dp)) Spacer(modifier = Modifier.height(26.dp))
when (selectedBetType) {
BetType.BINARY -> {
Column( Column(
modifier = Modifier.padding(vertical = 20.dp), modifier = Modifier.padding(vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(17.dp) verticalArrangement = Arrangement.spacedBy(17.dp)
) { ) {
when (selectedBetType) {
BetCreationViewModel.BetTypeState.Binary -> {
BetCreationScreenBottomText(text = stringResource(id = R.string.bet_creation_yes_no_bottom_text_1)) BetCreationScreenBottomText(text = stringResource(id = R.string.bet_creation_yes_no_bottom_text_1))
BetCreationScreenBottomText(text = stringResource(id = R.string.bet_creation_yes_no_bottom_text_2)) BetCreationScreenBottomText(text = stringResource(id = R.string.bet_creation_yes_no_bottom_text_2))
} }
is BetCreationViewModel.BetTypeState.Match -> {
BetCreationScreenBottomText(text = stringResource(selectedBetType.type.getTitleId()))
} }
BetType.MATCH -> { is BetCreationViewModel.BetTypeState.Custom -> {
BetCreationScreenBottomText( val (currentAnswer, setCurrentAnswer) = remember { mutableStateOf("") }
text = stringResource(selectedBetType.getTitleId())
BetCreationScreenBottomText(text = stringResource(id = R.string.bet_creation_custom_bottom_text_1))
BetCreationScreenBottomText(text = stringResource(id = R.string.bet_creation_custom_bottom_text_2))
BetCreationScreenCustomAnswerTextField(
value = currentAnswer,
setValue = setCurrentAnswer,
onAdd = { addAnswer(currentAnswer) },
enabled = selectedBetType.possibleAnswers.size < BET_MAX_ANSWERS,
buttonEnabled = currentAnswer.isNotBlank() && (currentAnswer !in selectedBetType.possibleAnswers),
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(
id = R.string.bet_creation_max_answers,
BET_MAX_ANSWERS - selectedBetType.possibleAnswers.size
),
color = AllInTheme.colors.onMainSurface,
style = AllInTheme.typography.sm1,
modifier = Modifier.align(Alignment.End)
) )
}
BetType.CUSTOM -> {
BetCreationScreenBottomText( FlowRow(
text = stringResource(selectedBetType.getTitleId()) horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
selectedBetType.possibleAnswers.fastForEach {
BetCreationScreenCustomAnswer(
text = it,
onDelete = { deleteAnswer(it) }
) )
} }
} }
} }
} }
typeError?.let { AllInErrorLine(text = it) }
}
}
}
} }
@Preview @Preview
@ -83,9 +128,28 @@ private fun BetCreationScreenAnswerTabPreview() {
AllInTheme { AllInTheme {
BetCreationScreenAnswerTab( BetCreationScreenAnswerTab(
selected = null, selected = null,
selectedBetType = BetType.BINARY, selectedBetType = BetCreationViewModel.BetTypeState.Binary,
setSelected = { },
elements = listOf(),
addAnswer = { },
deleteAnswer = { },
typeError = "Error"
)
}
}
@Preview
@Composable
private fun BetCreationScreenAnswerTabCustomPreview() {
AllInTheme {
BetCreationScreenAnswerTab(
selected = null,
selectedBetType = BetCreationViewModel.BetTypeState.Custom(listOf("Lorem ipsum", "Lorem iiiipsum", "Looooorem")),
setSelected = { }, setSelected = { },
elements = listOf() elements = listOf(),
addAnswer = { },
deleteAnswer = { },
typeError = null
) )
} }
} }

@ -22,16 +22,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
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 fr.iut.alldev.allin.R import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.data.model.User
import fr.iut.alldev.allin.ext.nonLinkedScroll
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenBottomText import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenBottomText
import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenFriendLine import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenFriendLine
@ -108,13 +105,7 @@ fun QuestionTabPrivacySection(
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.heightIn(max = 440.dp) .heightIn(max = 440.dp)
.nestedScroll(object : NestedScrollConnection { .nonLinkedScroll()
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
) = available.copy(x = 0f)
})
) { ) {
itemsIndexed(friends, key = { _, it -> it.id }) { idx, it -> itemsIndexed(friends, key = { _, it -> it.id }) { idx, it ->
var isSelected by remember { var isSelected by remember {

@ -27,13 +27,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -60,6 +56,7 @@ import fr.iut.alldev.allin.ext.bottomSheetNavigationBarsInsets
import fr.iut.alldev.allin.ext.formatToSimple import fr.iut.alldev.allin.ext.formatToSimple
import fr.iut.alldev.allin.ext.getDateEndLabelId import fr.iut.alldev.allin.ext.getDateEndLabelId
import fr.iut.alldev.allin.ext.getDateStartLabelId import fr.iut.alldev.allin.ext.getDateStartLabelId
import fr.iut.alldev.allin.ext.nonLinkedScroll
import fr.iut.alldev.allin.theme.AllInColorToken import fr.iut.alldev.allin.theme.AllInColorToken
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
import fr.iut.alldev.allin.ui.betStatus.components.BetStatusWinner import fr.iut.alldev.allin.ui.betStatus.components.BetStatusWinner
@ -134,13 +131,7 @@ class BetStatusBottomSheetBetDisplayer(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.background(AllInTheme.colors.background2) .background(AllInTheme.colors.background2)
.nestedScroll(object : NestedScrollConnection { .nonLinkedScroll(),
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
) = available.copy(x = 0f)
}),
contentPadding = bottomSheetNavigationBarsInsets().asPaddingValues(top = 20.dp, start = 20.dp, end = 20.dp) contentPadding = bottomSheetNavigationBarsInsets().asPaddingValues(top = 20.dp, start = 20.dp, end = 20.dp)
) { ) {

@ -8,6 +8,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -29,9 +30,10 @@ fun AllInButton(
enabled: Boolean = true, enabled: Boolean = true,
isSmall: Boolean = false, isSmall: Boolean = false,
radius: Dp = 10.dp, radius: Dp = 10.dp,
shape: Shape = AbsoluteSmoothCornerShape(radius, smoothnessAsPercent = 100)
) { ) {
Button( Button(
shape = AbsoluteSmoothCornerShape(radius, smoothnessAsPercent = 100), shape = shape,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = color, containerColor = color,
disabledContainerColor = AllInTheme.colors.disabled disabledContainerColor = AllInTheme.colors.disabled

@ -1,7 +1,6 @@
package fr.iut.alldev.allin.ui.core package fr.iut.alldev.allin.ui.core
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -59,7 +58,9 @@ fun AllInTextField(
imeAction: ImeAction = ImeAction.Default, imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default,
borderColor: Color = AllInTheme.colors.onBackground2, borderColor: Color = AllInTheme.colors.onBackground2,
disabledBorderColor: Color = AllInTheme.colors.disabledBorder,
containerColor: Color = AllInTheme.colors.background, containerColor: Color = AllInTheme.colors.background,
disabledContainerColor: Color = AllInTheme.colors.disabled,
textColor: Color = AllInTheme.colors.onMainSurface, textColor: Color = AllInTheme.colors.onMainSurface,
placeholderColor: Color = AllInTheme.colors.onBackground2, placeholderColor: Color = AllInTheme.colors.onBackground2,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
@ -117,12 +118,14 @@ fun AllInTextField(
cursorColor = textColor, cursorColor = textColor,
focusedBorderColor = borderColor, focusedBorderColor = borderColor,
unfocusedBorderColor = borderColor, unfocusedBorderColor = borderColor,
disabledBorderColor = disabledBorderColor,
focusedTextColor = textColor, focusedTextColor = textColor,
unfocusedTextColor = textColor, unfocusedTextColor = textColor,
errorTextColor = textColor, errorTextColor = textColor,
focusedContainerColor = containerColor, focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor, unfocusedContainerColor = containerColor,
errorContainerColor = containerColor, errorContainerColor = containerColor,
disabledContainerColor = disabledContainerColor,
) )
) )
} }
@ -251,7 +254,6 @@ fun AllInIntTextField(
} }
@OptIn(ExperimentalFoundationApi::class)
@Preview @Preview
@Composable @Composable
private fun AllInTextFieldPlaceholderPreview() { private fun AllInTextFieldPlaceholderPreview() {
@ -264,6 +266,18 @@ private fun AllInTextFieldPlaceholderPreview() {
} }
} }
@Preview
@Composable
private fun AllInTextFieldDisabledPreview() {
AllInTheme {
AllInTextField(
placeholder = "Email",
value = "",
onValueChange = { },
enabled = false
)
}
}
@Preview @Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

@ -34,6 +34,7 @@
<string name="field_error_already_used">%s est déjà utilisé.</string> <string name="field_error_already_used">%s est déjà utilisé.</string>
<string name="field_error_past_date">La %s doit être dans le futur.</string> <string name="field_error_past_date">La %s doit être dans le futur.</string>
<string name="field_error_date_order">La %s doit venir après %s.</string> <string name="field_error_date_order">La %s doit venir après %s.</string>
<string name="field_error_no_response">Le bet doit au moins avoir 2 réponses.</string>
<!--Drawer--> <!--Drawer-->
<string name="drawer_bets">Bets</string> <string name="drawer_bets">Bets</string>
@ -99,8 +100,11 @@
<string name="bet_creation_private_bottom_text_3">Vous pourrez inviter des amis à tout moment pendant la période dinscription.</string> <string name="bet_creation_private_bottom_text_3">Vous pourrez inviter des amis à tout moment pendant la période dinscription.</string>
<string name="bet_creation_yes_no_bottom_text_1">Les utilisateurs devront répondre au pari avec OUI ou NON.</string> <string name="bet_creation_yes_no_bottom_text_1">Les utilisateurs devront répondre au pari avec OUI ou NON.</string>
<string name="bet_creation_yes_no_bottom_text_2">Aucune autre réponse ne sera acceptée.</string> <string name="bet_creation_yes_no_bottom_text_2">Aucune autre réponse ne sera acceptée.</string>
<string name="bet_creation_custom_bottom_text_1">Vous allez renseiner les différentes réponses disponibles dans ce pari.</string>
<string name="bet_creation_custom_bottom_text_2">Faites attention à être clair et éviter toutes incertitudes.</string>
<string name="bet_creation_error">Erreur lors de la création du bet, veuillez rééssayer.</string> <string name="bet_creation_error">Erreur lors de la création du bet, veuillez rééssayer.</string>
<string name="bet_creation_success_message">Bet créé !</string> <string name="bet_creation_success_message">Bet créé !</string>
<string name="bet_creation_max_answers">encore %d max.</string>
<!--Bet Page--> <!--Bet Page-->
<string name="bet_popular">Populaire</string> <string name="bet_popular">Populaire</string>

@ -36,6 +36,7 @@
<string name="field_error_already_used">%s is already used.</string> <string name="field_error_already_used">%s is already used.</string>
<string name="field_error_past_date">The %s should be in the future.</string> <string name="field_error_past_date">The %s should be in the future.</string>
<string name="field_error_date_order">The %s should come after the %s.</string> <string name="field_error_date_order">The %s should come after the %s.</string>
<string name="field_error_no_response">The bet should at least have 2 answers.</string>
<!--Drawer--> <!--Drawer-->
<string name="drawer_bets">Bets</string> <string name="drawer_bets">Bets</string>
@ -100,9 +101,12 @@
<string name="bet_creation_private_bottom_text_2">Only your friends will be able to join the bet.</string> <string name="bet_creation_private_bottom_text_2">Only your friends will be able to join the bet.</string>
<string name="bet_creation_private_bottom_text_3">You can invite friends at any moment during the registration period.</string> <string name="bet_creation_private_bottom_text_3">You can invite friends at any moment during the registration period.</string>
<string name="bet_creation_yes_no_bottom_text_1">The participants will have to respond with either YES or NO.</string> <string name="bet_creation_yes_no_bottom_text_1">The participants will have to respond with either YES or NO.</string>
<string name="bet_creation_custom_bottom_text_1">You will fill in all the possible answers for this bet.</string>
<string name="bet_creation_custom_bottom_text_2">Make sure to be clear and to avoir any uncertainties.</string>
<string name="bet_creation_yes_no_bottom_text_2">No other answer will be accepted.</string> <string name="bet_creation_yes_no_bottom_text_2">No other answer will be accepted.</string>
<string name="bet_creation_error">Error while creating the bet. Please try again.</string> <string name="bet_creation_error">Error while creating the bet. Please try again.</string>
<string name="bet_creation_success_message">Bet created !</string> <string name="bet_creation_success_message">Bet created !</string>
<string name="bet_creation_max_answers">still %d max.</string>
<!--Bet Page--> <!--Bet Page-->
<string name="bet_popular">Popular</string> <string name="bet_popular">Popular</string>

Loading…
Cancel
Save