Correcting API errors, starting the displaying of bets and adding simple UI tests
continuous-integration/drone/push Build is passing Details

pull/3/head
Arthur VALIN 1 year ago
parent 6f559862e4
commit 038781de5a

@ -27,7 +27,7 @@ android {
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "fr.iut.alldev.allin.TestRunner"
vectorDrawables {
useSupportLibrary true
}
@ -76,13 +76,20 @@ dependencies {
implementation 'androidx.compose.material3:material3:1.2.0-alpha08'
implementation "androidx.compose.material:material:1.5.3"
implementation "androidx.navigation:navigation-compose:2.7.3"
implementation project(path: ':data')
testImplementation 'junit:junit:4.13.2'
implementation 'androidx.compose.material:material-icons-core'
implementation 'androidx.compose.material:material-icons-extended'
//Tests
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
implementation("androidx.core:core-splashscreen:1.0.1")
@ -90,6 +97,7 @@ dependencies {
//Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
implementation 'com.github.racra:smooth-corner-rect-android-compose:v1.0.0'

@ -1,24 +0,0 @@
package fr.iut.alldev.allin
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.iut.alldev.allin", appContext.packageName)
}
}

@ -0,0 +1,13 @@
package fr.iut.alldev.allin
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class TestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

@ -0,0 +1,7 @@
package fr.iut.alldev.allin.test.finders
import androidx.compose.ui.test.*
fun SemanticsNodeInteraction.onChildWith(matcher: SemanticsMatcher) = onChildren().filterToOne(matcher)
fun SemanticsNodeInteraction.onChildWithTag(testTag: String) = onChildWith(hasTestTag(testTag))

@ -0,0 +1,31 @@
package fr.iut.alldev.allin.test.mock
import fr.iut.alldev.allin.data.model.bet.BetStatus
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import java.time.ZonedDateTime
object Bets {
val bets by lazy{
listOf(
YesNoBet(
theme = "Theme",
phrase = "Phrase",
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.IN_PROGRESS
),
MatchBet(
theme = "Theme",
phrase = "Phrase",
endRegisterDate = ZonedDateTime.now(),
endBetDate = ZonedDateTime.now(),
isPublic = true,
betStatus = BetStatus.IN_PROGRESS,
nameTeam1 = "Team_1",
nameTeam2 = "Team_2"
),
)
}
}

@ -0,0 +1,66 @@
package fr.iut.alldev.allin.vo.bet
import androidx.activity.compose.setContent
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import fr.iut.alldev.allin.test.TestTags
import fr.iut.alldev.allin.test.mock.Bets
import fr.iut.alldev.allin.ui.MainActivity
import fr.iut.alldev.allin.ui.theme.AllInTheme
import fr.iut.alldev.allin.vo.bet.factory.toBetVO
import fr.iut.alldev.allin.vo.bet.visitor.BetTestVisitor
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class BetVOTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Before
fun init() {
hiltRule.inject()
}
companion object {
val visitor = BetTestVisitor()
}
@Test
fun testVisitor_shouldDisplayYesNoBetUI(){
//Given
//When
composeTestRule.activity.setContent {
AllInTheme{
Bets.bets[0].toBetVO()?.accept(v = visitor)
}
}
//Expect
composeTestRule.onNodeWithTag(TestTags.YES_NO_BET.tag).assertExists()
composeTestRule.onNodeWithTag(TestTags.MATCH_BET.tag).assertDoesNotExist()
}
@Test
fun testVisitor_shouldDisplayMatchUI(){
//Given
//When
composeTestRule.activity.setContent {
AllInTheme{
Bets.bets[1].toBetVO()?.accept(v = visitor)
}
}
//Expect
composeTestRule.onNodeWithTag(TestTags.MATCH_BET.tag).assertExists()
composeTestRule.onNodeWithTag(TestTags.YES_NO_BET.tag).assertDoesNotExist()
}
}

@ -0,0 +1,21 @@
package fr.iut.alldev.allin.vo.bet.visitor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.test.TestTags
class BetTestVisitor : DisplayBetVisitor {
@Composable
override fun visitYesNoBet(b: YesNoBet) {
Text("This is a YesNo Bet", Modifier.testTag(TestTags.YES_NO_BET.tag))
}
@Composable
override fun visitMatchBet(b: MatchBet) {
Text("This is a Match Bet", Modifier.testTag(TestTags.MATCH_BET.tag))
}
}

@ -14,7 +14,6 @@ annotation class AllInCurrentUser
@Module
@InstallIn(SingletonComponent::class)
internal object CurrentUserModule {
@AllInCurrentUser
@Provides
fun provideUser(

@ -3,7 +3,7 @@ package fr.iut.alldev.allin.ext
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.BetStatus
import fr.iut.alldev.allin.data.model.bet.BetStatus
import fr.iut.alldev.allin.ui.theme.AllInTheme
fun BetStatus.getTitle(): Int {

@ -6,7 +6,7 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.SportsSoccer
import androidx.compose.ui.graphics.vector.ImageVector
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.BetType
import fr.iut.alldev.allin.data.model.bet.BetType
fun BetType.getTitle(): Int {
return when (this) {

@ -25,6 +25,10 @@ sealed class FieldErrorState(
data class NoSpecialCharacter(val fieldName: String, val characters: String = ALLOWED_SYMBOLS) :
FieldErrorState(R.string.FieldError_NoSpecialCharacter, arrayOf(fieldName, characters))
data class AlreadyUsed(val value: String) :
FieldErrorState(R.string.FieldError_AlreadyUsed, arrayOf(value))
@Composable
fun errorResource() = stringResourceOrNull(id = messageId, messageArgs)
}

@ -0,0 +1,6 @@
package fr.iut.alldev.allin.test
enum class TestTags(val tag: String) {
YES_NO_BET("YES_NO"),
MATCH_BET("MATCH_BET")
}

@ -21,17 +21,31 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.BetStatus
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.ui.bet.components.BetScreenCard
import fr.iut.alldev.allin.ui.bet.components.BetScreenPopularCard
import fr.iut.alldev.allin.ui.core.AllInChip
import fr.iut.alldev.allin.ui.theme.AllInTheme
import java.time.ZonedDateTime
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class
private val bets = listOf(
YesNoBet(
"Études",
"Emre va t'il finir son TP de MAUI?",
ZonedDateTime.now(),
ZonedDateTime.now(),
true,
BetStatus.IN_PROGRESS
),
)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
fun BetScreen(
viewModel: BetViewModel = hiltViewModel(),
showBetStatus: ()->Unit
selectBet: (Bet)->Unit
){
val horizontalPadding = 23.dp
@ -100,7 +114,7 @@ fun BetScreen(
}
}
}
items(5){
items(bets){
BetScreenCard(
creator = "Lucas",
category = "Études",
@ -108,7 +122,7 @@ fun BetScreen(
date = "11 Sept.",
time = "13:00",
players = List(3){ null },
onClickParticipate = showBetStatus,
onClickParticipate = { selectBet(it) },
modifier = Modifier.padding(horizontal = horizontalPadding)
)
Spacer(modifier = Modifier.height(24.dp))

@ -13,7 +13,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.BetType
import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.ext.getIcon
import fr.iut.alldev.allin.ext.getTitle
import fr.iut.alldev.allin.ui.betcreation.tabs.BetCreationScreenAnswerTab

@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.BetType
import fr.iut.alldev.allin.data.model.bet.BetType
import fr.iut.alldev.allin.ext.getTitle
import fr.iut.alldev.allin.ui.betcreation.components.BetCreationScreenBottomText
import fr.iut.alldev.allin.ui.core.AllInSelectionBox

@ -3,15 +3,19 @@ package fr.iut.alldev.allin.ui.betstatus
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import fr.iut.alldev.allin.data.model.BetStatus
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.ui.betstatus.components.BetStatusBottomSheetBack
import fr.iut.alldev.allin.ui.betstatus.visitor.BetStatusBottomSheetDisplayBetVisitor
import fr.iut.alldev.allin.ui.core.AllInBottomSheet
import fr.iut.alldev.allin.vo.bet.BetVO
internal const val SHEET_HEIGHT = .85f
private val visitor = BetStatusBottomSheetDisplayBetVisitor()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -19,7 +23,7 @@ fun BetStatusBottomSheet(
state: SheetState,
sheetVisibility: Boolean,
sheetBackVisibility: Boolean,
betStatus: BetStatus,
bet: BetVO<Bet>?,
onDismiss: ()->Unit
) {
AnimatedVisibility(
@ -31,9 +35,11 @@ fun BetStatusBottomSheet(
targetOffsetY = { it }
)
) {
BetStatusBottomSheetBack(
status = betStatus
)
bet?.let {
BetStatusBottomSheetBack(
status = it.bet.betStatus
)
}
}
AllInBottomSheet(
@ -46,6 +52,7 @@ fun BetStatusBottomSheet(
Modifier
.fillMaxHeight(SHEET_HEIGHT)
) {
bet?.accept(visitor)
}
}
}

@ -16,7 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.BetStatus
import fr.iut.alldev.allin.data.model.bet.BetStatus
import fr.iut.alldev.allin.ext.getColor
import fr.iut.alldev.allin.ext.getTextColor
import fr.iut.alldev.allin.ext.getTitle
@ -45,7 +45,7 @@ fun BetStatusBottomSheetBack(
style = AllInTheme.typography.h2.copy(
fontStyle = FontStyle.Italic
),
fontSize = 20.sp,
fontSize = 30.sp,
modifier = Modifier.weight(1f)
)
Icon(

@ -0,0 +1,71 @@
package fr.iut.alldev.allin.ui.betstatus.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.ext.toPercentageString
import fr.iut.alldev.allin.ui.core.PercentagePositionnedElement
import fr.iut.alldev.allin.ui.core.StatBar
import fr.iut.alldev.allin.ui.theme.AllInTheme
@Composable
fun YesNoStatBar(
yesPercentage: Float
) {
Column(
Modifier.padding(horizontal = 9.dp)
){
Row{
Text(
text = stringResource(id = R.string.Yes).uppercase(),
color = AllInTheme.colors.allIn_Blue,
style = AllInTheme.typography.h1,
fontStyle = FontStyle.Italic,
fontSize = 30.sp,
modifier = Modifier.weight(1f)
)
Text(
text = stringResource(id = R.string.No).uppercase(),
style = AllInTheme.typography.h1,
fontStyle = FontStyle.Italic,
fontSize = 30.sp,
color = AllInTheme.colors.allIn_BarPink
)
}
StatBar(percentage = yesPercentage)
PercentagePositionnedElement(
percentage = yesPercentage
){
Text(
text = yesPercentage.toPercentageString(),
style = AllInTheme.typography.h3,
color = AllInTheme.colors.allIn_BarPurple
)
}
}
}
private class YesNoStatBarPreviewProvider: PreviewParameterProvider<Float> {
override val values = sequenceOf(0f, .33f, .5f, .66f, 1f)
}
@Preview
@Composable
private fun YesNoStatBarPreview(
@PreviewParameter(YesNoStatBarPreviewProvider::class) percentage: Float
) {
AllInTheme {
YesNoStatBar(percentage)
}
}

@ -0,0 +1,33 @@
package fr.iut.alldev.allin.ui.betstatus.visitor
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.ui.core.StatBar
import fr.iut.alldev.allin.ui.core.bet.BetTitleHeader
import fr.iut.alldev.allin.vo.bet.visitor.DisplayBetVisitor
class BetStatusBottomSheetDisplayBetVisitor : DisplayBetVisitor {
@Composable
override fun visitYesNoBet(b: YesNoBet) {
Column {
BetTitleHeader(
title = b.phrase,
category = b.theme,
creator = "Lucas" /*TODO : Creator*/,
modifier = Modifier.padding(horizontal = 20.dp)
)
StatBar(percentage = .86f)
}
}
@Composable
override fun visitMatchBet(b: MatchBet) {
Text("This is a MATCH BET")
}
}

@ -15,10 +15,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
@ -45,6 +42,7 @@ fun AllInTextField(
bringIntoViewRequester: BringIntoViewRequester,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
borderColor: Color = AllInTheme.themeColors.on_background_2,
containerColor: Color = AllInTheme.themeColors.background,
@ -114,7 +112,7 @@ fun AllInTextField(
},
textStyle = AllInTheme.typography.r,
enabled = enabled,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
keyboardActions = keyboardActions,
shape = AbsoluteSmoothCornerShape(10.dp, 100),
colors = OutlinedTextFieldDefaults.colors(
@ -137,6 +135,7 @@ fun AllInPasswordField(
placeholder: String,
value: String,
modifier: Modifier = Modifier,
imeAction: ImeAction = ImeAction.Default,
keyboardType: KeyboardType = KeyboardType.Password,
keyboardActions: KeyboardActions = KeyboardActions.Default,
errorText: String? = null,
@ -151,6 +150,7 @@ fun AllInPasswordField(
modifier = modifier,
errorText = errorText,
placeholder = placeholder,
imeAction = imeAction,
keyboardActions = keyboardActions,
visualTransformation = if (hidden) PasswordVisualTransformation() else VisualTransformation.None,
value = value,

@ -0,0 +1,53 @@
package fr.iut.alldev.allin.ui.core
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.ui.theme.AllInTheme
@Composable
fun PercentagePositionnedElement(
percentage: Float,
offset: Dp = (-9).dp,
content: @Composable ()->Unit
) {
Box(
Modifier.fillMaxWidth()
) {
when (percentage) {
0f -> {
content()
}
1f -> {
Box(
Modifier.align(Alignment.CenterEnd)
){
content()
}
}
else -> {
Row {
Spacer(modifier = Modifier.fillMaxWidth(percentage))
Box(Modifier.offset(x = offset)) {
content()
}
}
}
}
}
}
@Preview
@Composable
private fun PercentagePositionnedElementPreview() {
AllInTheme {
PercentagePositionnedElement(percentage = .4f) {
Text("MyElement")
}
}
}

@ -5,17 +5,13 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.ui.theme.AllInTheme
@ -25,9 +21,7 @@ fun StatBar(
) {
val radius100percent = if(percentage==1f) 50 else 0
val radius0percent = if(percentage==0f) 50 else 0
Box(
Modifier.padding(horizontal = 9.dp)
){
Box{
Row(
Modifier.align(Alignment.Center)
){
@ -45,18 +39,7 @@ fun StatBar(
)
.background(AllInTheme.colors.allIn_Bar1stGradient)
){
Text(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 15.dp),
text = "OUI",
style = AllInTheme.typography.h2,
textAlign = TextAlign.Center,
fontSize = 25.sp,
color = Color.White.copy(alpha = 0.3f),
)
}
)
if(percentage!=0f && percentage!=1f) {
Spacer(modifier = Modifier.width(15.dp))
}
@ -75,44 +58,27 @@ fun StatBar(
.background(AllInTheme.colors.allIn_Bar2ndGradient)
)
}
Box(
Modifier
.fillMaxWidth()
.align(Alignment.Center)
) {
when (percentage) {
0f -> {
Icon(
painter = painterResource(id = R.drawable.fire_solid),
tint = AllInTheme.colors.allIn_BarPink,
contentDescription = null,
modifier = Modifier
.size(32.dp)
)
}
1f -> {
Icon(
painter = painterResource(id = R.drawable.fire_solid),
tint = AllInTheme.colors.allIn_BarPurple,
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterEnd)
.size(32.dp)
PercentagePositionnedElement(percentage = percentage) {
when(percentage){
0f -> Icon(
painter = painterResource(id = R.drawable.fire_solid),
tint = AllInTheme.colors.allIn_BarPink,
contentDescription = null,
modifier = Modifier.size(32.dp)
)
}
else -> {
Row {
Spacer(modifier = Modifier.fillMaxWidth(percentage))
Image(
painter = painterResource(id = R.drawable.bar_flame),
contentDescription = null,
modifier = Modifier
.size(32.dp)
.offset(x = (-9).dp)
)
}
}
1f -> Icon(
painter = painterResource(id = R.drawable.fire_solid),
tint = AllInTheme.colors.allIn_BarPurple,
contentDescription = null,
modifier = Modifier.size(32.dp)
)
else -> Image(
painter = painterResource(id = R.drawable.bar_flame),
contentDescription = null,
modifier = Modifier.size(32.dp)
)
}
}
}
}

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
@ -16,9 +17,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -34,6 +38,7 @@ fun LoginScreen(
navigateToRegister: ()->Unit,
loginViewModel: LoginViewModel = hiltViewModel()
) {
val focusManager = LocalFocusManager.current
val bringIntoViewRequester = BringIntoViewRequester()
val loading by remember{ loginViewModel.loading }
@ -42,6 +47,18 @@ fun LoginScreen(
val (username, setUsername) = remember{ loginViewModel.username }
val (password, setPassword) = remember{ loginViewModel.password }
val keyboardActions = remember {
KeyboardActions(
onDone = {
focusManager.clearFocus()
loginViewModel.onLogin(navigateToDashboard)
},
onNext = {
focusManager.moveFocus(FocusDirection.Down)
}
)
}
Box(
Modifier
.fillMaxSize()
@ -78,14 +95,18 @@ fun LoginScreen(
placeholder = stringResource(id = R.string.username),
value = username,
onValueChange = setUsername,
bringIntoViewRequester = bringIntoViewRequester
bringIntoViewRequester = bringIntoViewRequester,
imeAction = ImeAction.Next,
keyboardActions = keyboardActions
)
AllInPasswordField(
modifier = Modifier.fillMaxWidth(),
placeholder = stringResource(id = R.string.password),
value = password,
onValueChange = setPassword,
bringIntoViewRequester = bringIntoViewRequester
bringIntoViewRequester = bringIntoViewRequester,
imeAction = ImeAction.Done,
keyboardActions = keyboardActions
)
}
ClickableText(

@ -4,6 +4,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.api.interceptors.AllInAPIException
import fr.iut.alldev.allin.data.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -29,7 +30,7 @@ class LoginViewModel @Inject constructor(
withContext(Dispatchers.IO) {
try{
userRepository.login(username.value, password.value)
} catch (e: retrofit2.HttpException){
} catch (e: AllInAPIException){
hasError.value = true
}
}

@ -10,7 +10,6 @@ import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import fr.iut.alldev.allin.data.model.BetStatus
import fr.iut.alldev.allin.ui.betstatus.BetStatusBottomSheet
import fr.iut.alldev.allin.ui.main.components.AllInScaffold
import fr.iut.alldev.allin.ui.navigation.AllInDrawerNavHost
@ -19,6 +18,7 @@ import fr.iut.alldev.allin.ui.navigation.TopLevelDestination
import fr.iut.alldev.allin.ui.navigation.drawer.AllInDrawer
import fr.iut.alldev.allin.ui.navigation.popUpTo
import fr.iut.alldev.allin.ui.theme.AllInTheme
import fr.iut.alldev.allin.vo.bet.factory.toBetVO
import kotlinx.coroutines.launch
private val topLevelDestinations = listOf(
@ -59,6 +59,10 @@ fun MainScreen(
mainViewModel.currentUser
}
val (selectedBet, setSelectedBet) = remember{
mainViewModel.selectedBet
}
val scope = rememberCoroutineScope()
val (statusVisibility, sheetBackVisibility, setStatusVisibility)
@ -100,7 +104,10 @@ fun MainScreen(
) {
AllInDrawerNavHost(
navController = navController,
setStatusVisibility = setStatusVisibility
selectBet = {
setSelectedBet(it)
setStatusVisibility(true)
}
)
}
}
@ -113,9 +120,8 @@ fun MainScreen(
onDismiss = {
setStatusVisibility(false)
},
betStatus = BetStatus.IN_PROGRESS
bet = selectedBet?.toBetVO()
)
BackHandler(
enabled = drawerState.isOpen
) {

@ -3,8 +3,8 @@ package fr.iut.alldev.allin.ui.main
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.di.AllInCurrentUser
import fr.iut.alldev.allin.data.model.BetStatus
import fr.iut.alldev.allin.data.model.User
import javax.inject.Inject
@ -13,5 +13,5 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(
@AllInCurrentUser val currentUser: User
) : ViewModel() {
val selectedBet = mutableStateOf<BetStatus?>(null)
val selectedBet = mutableStateOf<Bet?>(null)
}

@ -11,6 +11,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.ui.bet.BetScreen
import fr.iut.alldev.allin.ui.betcreation.BetCreationScreen
import fr.iut.alldev.allin.ui.login.LoginScreen
@ -80,7 +81,7 @@ fun AllInNavHost(modifier: Modifier = Modifier,
internal fun AllInDrawerNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
setStatusVisibility: (Boolean) -> Unit,
selectBet: (Bet) -> Unit,
startDestination: String = Routes.PUBLIC_BETS
) {
NavHost(
@ -92,7 +93,7 @@ internal fun AllInDrawerNavHost(
) {
composable(route = Routes.PUBLIC_BETS) {
BetScreen(
showBetStatus = { setStatusVisibility(true) }
selectBet = selectBet
)
}
composable(route = Routes.BET_CREATION) {

@ -5,12 +5,16 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -31,6 +35,9 @@ fun RegisterScreen(
navigateToLogin: () -> Unit,
registerViewModel: RegisterViewModel = hiltViewModel(),
) {
val focusManager = LocalFocusManager.current
val loading by remember{ registerViewModel.loading }
val usernameError by remember{ registerViewModel.usernameError }
@ -50,6 +57,23 @@ fun RegisterScreen(
val emailFieldName = stringResource(id = R.string.email)
val passwordFieldName = stringResource(id = R.string.password)
val keyboardActions = remember {
KeyboardActions(
onDone = {
focusManager.clearFocus()
registerViewModel.onRegister(
usernameFieldName,
emailFieldName,
passwordFieldName,
navigateToDashboard
)
},
onNext = {
focusManager.moveFocus(FocusDirection.Down)
}
)
}
Column(
Modifier
.fillMaxSize()
@ -100,7 +124,9 @@ fun RegisterScreen(
onValueChange = setUsername,
maxChar = 20,
errorText = usernameError.errorResource(),
bringIntoViewRequester = bringIntoViewRequester
bringIntoViewRequester = bringIntoViewRequester,
imeAction = ImeAction.Next,
keyboardActions = keyboardActions
)
AllInTextField(
modifier = Modifier.fillMaxWidth(),
@ -109,7 +135,9 @@ fun RegisterScreen(
onValueChange = setEmail,
errorText = emailError.errorResource(),
keyboardType = KeyboardType.Email,
bringIntoViewRequester = bringIntoViewRequester
bringIntoViewRequester = bringIntoViewRequester,
imeAction = ImeAction.Next,
keyboardActions = keyboardActions
)
AllInPasswordField(
modifier = Modifier.fillMaxWidth(),
@ -117,7 +145,9 @@ fun RegisterScreen(
value = password,
errorText = passwordError.errorResource(),
onValueChange = setPassword,
bringIntoViewRequester = bringIntoViewRequester
bringIntoViewRequester = bringIntoViewRequester,
imeAction = ImeAction.Next,
keyboardActions = keyboardActions
)
AllInPasswordField(
modifier = Modifier.fillMaxWidth(),
@ -125,7 +155,9 @@ fun RegisterScreen(
value = passwordValidation,
errorText = passwordValidationError.errorResource(),
onValueChange = setPasswordValidation,
bringIntoViewRequester = bringIntoViewRequester
bringIntoViewRequester = bringIntoViewRequester,
imeAction = ImeAction.Done,
keyboardActions = keyboardActions
)
}
}

@ -4,6 +4,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.api.interceptors.AllInAPIException
import fr.iut.alldev.allin.data.repository.UserRepository
import fr.iut.alldev.allin.ext.ALLOWED_SYMBOLS
import fr.iut.alldev.allin.ext.FieldErrorState
@ -89,11 +90,17 @@ class RegisterViewModel @Inject constructor(
passwordFieldName.lowercase()
)
if(!hasError.value) {
userRepository.register(
username.value,
email.value,
password.value
)
try {
userRepository.register(
username.value,
email.value,
password.value
)
}catch(e : AllInAPIException){
usernameError.value = FieldErrorState.AlreadyUsed(username.value)
emailError.value = FieldErrorState.AlreadyUsed(email.value)
hasError.value = true
}
}
}
if(!hasError.value){

@ -0,0 +1,8 @@
package fr.iut.alldev.allin.vo
import androidx.compose.runtime.Composable
interface ViewObject<V>{
@Composable
fun accept(v: V)
}

@ -0,0 +1,28 @@
package fr.iut.alldev.allin.vo.bet
import androidx.compose.runtime.Composable
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.vo.ViewObject
import fr.iut.alldev.allin.vo.bet.visitor.DisplayBetVisitor
abstract class BetVO<T: Bet>(val bet: T)
: ViewObject<DisplayBetVisitor> {
@Composable
abstract override fun accept(v: DisplayBetVisitor)
}
class YesNoBetVO(bet: YesNoBet) : BetVO<YesNoBet>(bet){
@Composable
override fun accept(v: DisplayBetVisitor){
v.visitYesNoBet(b = bet)
}
}
class MatchBetVO(bet: MatchBet) : BetVO<MatchBet>(bet){
@Composable
override fun accept(v: DisplayBetVisitor){
v.visitMatchBet(b = bet)
}
}

@ -0,0 +1,30 @@
package fr.iut.alldev.allin.vo.bet.factory
import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
import fr.iut.alldev.allin.vo.bet.BetVO
import fr.iut.alldev.allin.vo.bet.MatchBetVO
import fr.iut.alldev.allin.vo.bet.YesNoBetVO
private val betTypeToVOMap = mapOf(
YesNoBet::class.java to YesNoBetVOFactory(),
MatchBet::class.java to MatchBetVOFactory()
)
abstract class BetVOFactory<out T : Bet> {
abstract fun create(bet: @UnsafeVariance T): BetVO<@UnsafeVariance T>
}
class YesNoBetVOFactory : BetVOFactory<YesNoBet>() {
override fun create(bet: YesNoBet) =
YesNoBetVO(bet)
}
class MatchBetVOFactory : BetVOFactory<MatchBet>() {
override fun create(bet: MatchBet) =
MatchBetVO(bet)
}
fun Bet.toBetVO() =
betTypeToVOMap[this.javaClass]?.create(this)

@ -0,0 +1,14 @@
package fr.iut.alldev.allin.vo.bet.visitor
import androidx.compose.runtime.Composable
import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.YesNoBet
interface DisplayBetVisitor {
@Composable
fun visitYesNoBet(b: YesNoBet)
@Composable
fun visitMatchBet(b: MatchBet)
}

@ -17,6 +17,9 @@
<string name="FieldError_BadFormat">Le %s a un mauvais format : %s.</string>
<string name="FieldError_NotIdentical">Les champs ne sont pas identiques.</string>
<string name="FieldError_NoSpecialCharacter">Le %s doit contenir au moins un caractère spécial : %s.</string>
<string name="FieldError_AlreadyUsed">%s est déjà utilisé.</string>
<string name="Yes">Oui</string>
<string name="No">Non</string>
<!--Drawer-->
<string name="bets">Bets</string>

@ -19,7 +19,10 @@
<string name="FieldError_BadFormat">The %s has bad format : %s.</string>
<string name="FieldError_NotIdentical">The fields are not identical.</string>
<string name="FieldError_NoSpecialCharacter">The %s must contain at least one special character : %s.</string>
<string name="FieldError_AlreadyUsed">%s is already used.</string>
<string name="Yes">Yes</string>
<string name="No">No</string>
<!--Drawer-->
<string name="bets">Bets</string>
<string name="best_win">Best win</string>

@ -55,6 +55,8 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
//Tests
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@ -66,7 +68,7 @@ dependencies {
// Retrofit
api "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.okhttp3:okhttp:4.11.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
debugImplementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
}

@ -4,6 +4,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import fr.iut.alldev.allin.data.api.interceptors.ErrorInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import javax.inject.Singleton
@ -21,5 +22,6 @@ internal object DebugNetworkModule {
level = HttpLoggingInterceptor.Level.BODY
}
)
.addInterceptor(ErrorInterceptor())
.build()
}

@ -5,7 +5,6 @@ import fr.iut.alldev.allin.data.api.model.ResponseUser
import retrofit2.http.Body
import retrofit2.http.POST
interface AllInApi {
@POST("users/login")
suspend fun login(

@ -0,0 +1,30 @@
package fr.iut.alldev.allin.data.api.interceptors
import okhttp3.Interceptor
import okhttp3.Response
import okio.IOException
open class AllInAPIException(message: String) : IOException(message)
class AllInNotFoundException(message: String) : AllInAPIException(message)
class AllInUnauthorizedException(message: String) : AllInAPIException(message)
class AllInUnsuccessfulException(message: String) : AllInAPIException(message)
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if(!response.isSuccessful){
when (response.code) {
404 -> throw AllInNotFoundException(response.message)
401 -> throw AllInUnauthorizedException(response.message)
else -> throw AllInUnsuccessfulException(response.message)
}
}
if (response.body?.contentType()?.subtype != "json") {
throw AllInAPIException(response.message)
}
return response
}
}

@ -23,6 +23,6 @@ data class ResponseUser(
@Keep
@Serializable
data class CheckUser(
val username: String,
val login: String,
val password: String,
)

@ -13,7 +13,6 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class ApiModule {
@Provides
@Singleton
fun provideAllInApi(@AllInUrl url: HttpUrl, okHttpClient: OkHttpClient): AllInApi {

@ -5,6 +5,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -32,10 +33,13 @@ internal object NetworkModule {
@Provides
fun provideUrl(): HttpUrl = "https://codefirst.iut.uca.fr/containers/AllDev-api/".toHttpUrl()
@OptIn(ExperimentalSerializationApi::class)
fun createRetrofit(url: HttpUrl, okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addConverterFactory(
json.asConverterFactory("application/json".toMediaType())
)
.baseUrl(url)
.build()
}
}

@ -0,0 +1,3 @@
package fr.iut.alldev.allin.data.ext
fun Float.toPercentageString(precision: Int = 0) = String.format("%.${precision}f", this*100) + "%"

@ -0,0 +1,12 @@
package fr.iut.alldev.allin.data.model.bet
import java.time.ZonedDateTime
abstract class Bet(
val theme: String,
val phrase: String,
val endRegisterDate: ZonedDateTime,
val endBetDate: ZonedDateTime,
val isPublic: Boolean,
val betStatus: BetStatus,
)

@ -1,4 +1,4 @@
package fr.iut.alldev.allin.data.model
package fr.iut.alldev.allin.data.model.bet
enum class BetStatus {
FINISHED,

@ -1,4 +1,4 @@
package fr.iut.alldev.allin.data.model
package fr.iut.alldev.allin.data.model.bet
enum class BetType {
YES_NO,

@ -0,0 +1,21 @@
package fr.iut.alldev.allin.data.model.bet
import java.time.ZonedDateTime
class MatchBet(
theme: String,
phrase: String,
endRegisterDate: ZonedDateTime,
endBetDate: ZonedDateTime,
isPublic: Boolean,
betStatus: BetStatus,
val nameTeam1: String,
val nameTeam2: String
) : Bet(
theme,
phrase,
endRegisterDate,
endBetDate,
isPublic,
betStatus
)

@ -0,0 +1,19 @@
package fr.iut.alldev.allin.data.model.bet
import java.time.ZonedDateTime
class YesNoBet(
theme: String,
phrase: String,
endRegisterDate: ZonedDateTime,
endBetDate: ZonedDateTime,
isPublic: Boolean,
betStatus: BetStatus
) : Bet(
theme,
phrase,
endRegisterDate,
endBetDate,
isPublic,
betStatus
)

@ -13,12 +13,13 @@ class UserRepositoryImpl @Inject constructor(
override suspend fun login(username: String, password: String) {
currentUser = api.login(
CheckUser(
username = username,
login = username,
password = password
)
).toUser()
}
override suspend fun register(username: String, email: String, password: String) {
currentUser = api.register(
ResponseUser(
@ -28,5 +29,6 @@ class UserRepositoryImpl @Inject constructor(
nbCoins = 0
)
).toUser()
}
}

@ -16,5 +16,6 @@ internal object ReleaseNetworkModule {
@Singleton
fun provideOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(ErrorInterceptor())
.build()
}
Loading…
Cancel
Save