Add profile and profile picture selection
continuous-integration/drone/push Build is passing Details

pull/5/head
avalin 11 months ago
parent c53f9e8b91
commit bcc735f713

@ -24,6 +24,17 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application> </application>
</manifest> </manifest>

@ -75,7 +75,8 @@ import fr.iut.alldev.allin.vo.bet.BetDisplayer
import java.util.Locale import java.util.Locale
class BetStatusBottomSheetBetDisplayer( class BetStatusBottomSheetBetDisplayer(
val openParticipateSheet: () -> Unit val openParticipateSheet: () -> Unit,
val getImageUrl: (id: String) -> String
) : BetDisplayer { ) : BetDisplayer {
@Composable @Composable
private fun DisplayBetDail( private fun DisplayBetDail(
@ -154,7 +155,7 @@ class BetStatusBottomSheetBetDisplayer(
BetStatusParticipant( BetStatusParticipant(
username = it.username, username = it.username,
allCoinsAmount = it.stake, allCoinsAmount = it.stake,
image = null // TODO : Image image = getImageUrl(it.id)
) )
HorizontalDivider( HorizontalDivider(
color = AllInTheme.colors.border, color = AllInTheme.colors.border,
@ -167,7 +168,7 @@ class BetStatusBottomSheetBetDisplayer(
BetStatusParticipant( BetStatusParticipant(
username = it.username, username = it.username,
allCoinsAmount = it.stake, allCoinsAmount = it.stake,
image = null // TODO : Image image = getImageUrl(it.id)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
@ -413,7 +414,8 @@ private fun BetStatusBottomSheetPreview(
) { ) {
AllInTheme { AllInTheme {
BetStatusBottomSheetBetDisplayer( BetStatusBottomSheetBetDisplayer(
openParticipateSheet = {} openParticipateSheet = {},
getImageUrl = { "" }
).DisplayBet( ).DisplayBet(
betDetail = bet, betDetail = bet,
currentUser = User( currentUser = User(

@ -53,7 +53,7 @@ fun ProfilePicture(
modifier = Modifier modifier = Modifier
.width(300.dp) .width(300.dp)
.height(174.dp) .height(174.dp)
.clip(RoundedCornerShape(14.dp)), .clip(shape),
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current)
.data(image) .data(image)
.crossfade(true) .crossfade(true)
@ -96,14 +96,3 @@ private fun ProfilePictureDefaultPreview() {
ProfilePicture(image = null, fallback = "LS") ProfilePicture(image = null, fallback = "LS")
} }
} }
@Preview
@Composable
private fun ProfilePicturePreview() {
AllInTheme {
ProfilePicture(
fallback = "LS",
image = "https://cdn.myanimelist.net/s/common/userimages/6076ae8b-54ed-4924-bb81-4d2d51806b1a_225w?s=965262aa50355e917a7ef9579c58fffc"
)
}
}

@ -57,7 +57,7 @@ private val topLevelDestinations = listOf(
TopLevelDestination.CurrentBets, TopLevelDestination.CurrentBets,
TopLevelDestination.BetHistory, TopLevelDestination.BetHistory,
TopLevelDestination.Friends, TopLevelDestination.Friends,
TopLevelDestination.Ranking, TopLevelDestination.Ranking
) )
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -113,7 +113,8 @@ fun MainScreen(
) )
val betStatusDisplayer = remember { val betStatusDisplayer = remember {
BetStatusBottomSheetBetDisplayer( BetStatusBottomSheetBetDisplayer(
openParticipateSheet = { setParticipateSheetVisibility(true) } openParticipateSheet = { setParticipateSheetVisibility(true) },
getImageUrl = { mainViewModel.getImageUrl(it) }
) )
} }
@ -123,7 +124,7 @@ fun MainScreen(
AllInDrawer( AllInDrawer(
drawerState = drawerState, drawerState = drawerState,
destinations = topLevelDestinations, destinations = topLevelDestinations,
scope = scope, id = currentUser?.id ?: "",
username = currentUser?.username ?: "", username = currentUser?.username ?: "",
nbFriends = currentUser?.nbFriends ?: 0, nbFriends = currentUser?.nbFriends ?: 0,
nbBets = currentUser?.nbBets ?: 0, nbBets = currentUser?.nbBets ?: 0,
@ -131,6 +132,7 @@ fun MainScreen(
image = currentUser?.image, image = currentUser?.image,
navigateTo = { route -> navigateTo = { route ->
mainViewModel.fetchEvents() mainViewModel.fetchEvents()
scope.launch { drawerState.close() }
navController.popUpTo(route, startDestination) navController.popUpTo(route, startDestination)
}, },
logout = { logout = {

@ -118,6 +118,8 @@ class MainViewModel @Inject constructor(
} }
} }
fun getImageUrl(id: String) = userRepository.getImageUrl(id)
fun participateToBet(stake: Int, response: String) { fun participateToBet(stake: Int, response: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -127,6 +129,7 @@ class MainViewModel @Inject constructor(
selectedBet.value?.let { selectedBet.value?.let {
val participation = Participation( val participation = Participation(
betId = it.bet.id, betId = it.bet.id,
id = user.id,
username = user.username, username = user.username,
response = response, response = response,
stake = stake stake = stake

@ -14,9 +14,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import fr.iut.alldev.allin.R import fr.iut.alldev.allin.R
import fr.iut.alldev.allin.data.model.bet.Bet import fr.iut.alldev.allin.data.model.bet.Bet
import fr.iut.alldev.allin.theme.AllInTheme import fr.iut.alldev.allin.theme.AllInTheme
@ -29,6 +31,7 @@ import fr.iut.alldev.allin.ui.friends.FriendsScreen
import fr.iut.alldev.allin.ui.login.LoginScreen import fr.iut.alldev.allin.ui.login.LoginScreen
import fr.iut.alldev.allin.ui.main.MainScreen import fr.iut.alldev.allin.ui.main.MainScreen
import fr.iut.alldev.allin.ui.main.MainViewModel import fr.iut.alldev.allin.ui.main.MainViewModel
import fr.iut.alldev.allin.ui.profile.ProfileScreen
import fr.iut.alldev.allin.ui.ranking.RankingScreen import fr.iut.alldev.allin.ui.ranking.RankingScreen
import fr.iut.alldev.allin.ui.register.RegisterScreen import fr.iut.alldev.allin.ui.register.RegisterScreen
import fr.iut.alldev.allin.ui.splash.SplashScreen import fr.iut.alldev.allin.ui.splash.SplashScreen
@ -46,6 +49,7 @@ object Routes {
const val BET_CURRENT = "BET_CURRENT" const val BET_CURRENT = "BET_CURRENT"
const val FRIENDS = "FRIENDS" const val FRIENDS = "FRIENDS"
const val RANKING = "RANKING" const val RANKING = "RANKING"
const val PROFILE = "PROFILE"
} }
object Arguments { object Arguments {
@ -165,6 +169,14 @@ internal fun AllInDrawerNavHost(
backHandlers() backHandlers()
BetCurrentScreen(selectBet = selectBet) BetCurrentScreen(selectBet = selectBet)
} }
composable(
route = "${Routes.PROFILE}/{${Arguments.USER_ID}}",
arguments = listOf(navArgument(Arguments.USER_ID) { type = NavType.StringType })
) {
backHandlers()
ProfileScreen()
}
} }
} }

@ -24,17 +24,16 @@ import androidx.compose.ui.unit.sp
import fr.iut.alldev.allin.R import fr.iut.alldev.allin.R
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.navigation.Routes
import fr.iut.alldev.allin.ui.navigation.TopLevelDestination import fr.iut.alldev.allin.ui.navigation.TopLevelDestination
import fr.iut.alldev.allin.ui.navigation.drawer.components.DrawerCell import fr.iut.alldev.allin.ui.navigation.drawer.components.DrawerCell
import fr.iut.alldev.allin.ui.navigation.drawer.components.DrawerHeader import fr.iut.alldev.allin.ui.navigation.drawer.components.DrawerHeader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable @Composable
fun AllInDrawer( fun AllInDrawer(
drawerState: DrawerState, drawerState: DrawerState,
destinations: List<TopLevelDestination>, destinations: List<TopLevelDestination>,
scope: CoroutineScope, id: String,
username: String, username: String,
nbBets: Int, nbBets: Int,
bestWin: Int, bestWin: Int,
@ -42,7 +41,7 @@ fun AllInDrawer(
image: String?, image: String?,
navigateTo: (String) -> Unit, navigateTo: (String) -> Unit,
logout: () -> Unit, logout: () -> Unit,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
@ -57,17 +56,15 @@ fun AllInDrawer(
nbFriends = nbFriends, nbFriends = nbFriends,
username = username, username = username,
image = image, image = image,
modifier = Modifier.padding(top = 39.dp, bottom = 26.dp) modifier = Modifier.padding(top = 39.dp, bottom = 26.dp),
navigateToProfile = { navigateTo("${Routes.PROFILE}/$id") }
) )
destinations.forEach { item -> destinations.forEach { item ->
DrawerCell( DrawerCell(
title = stringResource(item.title).uppercase(), title = stringResource(item.title).uppercase(),
subtitle = stringResource(item.subtitle), subtitle = stringResource(item.subtitle),
emoji = painterResource(id = item.emoji), emoji = painterResource(id = item.emoji),
onClick = { onClick = { navigateTo(item.route) },
scope.launch { drawerState.close() }
navigateTo(item.route)
},
modifier = Modifier.padding(vertical = 5.dp, horizontal = 13.dp) modifier = Modifier.padding(vertical = 5.dp, horizontal = 13.dp)
) )
} }

@ -1,5 +1,7 @@
package fr.iut.alldev.allin.ui.navigation.drawer.components package fr.iut.alldev.allin.ui.navigation.drawer.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Row import androidx.compose.foundation.layout.Row
@ -9,6 +11,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.res.stringResource import androidx.compose.ui.res.stringResource
@ -30,7 +33,9 @@ fun DrawerHeader(
username: String, username: String,
image: String?, image: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navigateToProfile: () -> Unit
) { ) {
val interactionSource = remember { MutableInteractionSource() }
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@ -38,7 +43,12 @@ fun DrawerHeader(
ProfilePicture( ProfilePicture(
image = image, image = image,
fallback = username.asFallbackProfileUsername(), fallback = username.asFallbackProfileUsername(),
borderWidth = 1.dp borderWidth = 1.dp,
modifier = Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = navigateToProfile
)
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text( Text(
@ -72,7 +82,8 @@ private fun DrawerHeaderPreview() {
bestWin = 360, bestWin = 360,
nbFriends = 5, nbFriends = 5,
username = "Pseudo", username = "Pseudo",
image = null image = null,
navigateToProfile = { }
) )
} }
} }

@ -1,12 +1,12 @@
package fr.iut.alldev.allin.ui.preview package fr.iut.alldev.allin.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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.CustomBet
import fr.iut.alldev.allin.data.model.bet.MatchBet import fr.iut.alldev.allin.data.model.bet.MatchBet
import fr.iut.alldev.allin.data.model.bet.NO_VALUE import fr.iut.alldev.allin.data.model.bet.NO_VALUE
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.YES_VALUE import fr.iut.alldev.allin.data.model.bet.YES_VALUE
import fr.iut.alldev.allin.data.model.bet.BinaryBet
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
@ -44,6 +44,7 @@ class BetDetailPreviewProvider : PreviewParameterProvider<BetDetail> {
odds = 1.0f odds = 1.0f
) )
) )
is MatchBet -> listOf( is MatchBet -> listOf(
BetAnswerDetail( BetAnswerDetail(
response = "The Monarchs", response = "The Monarchs",
@ -60,6 +61,7 @@ class BetDetailPreviewProvider : PreviewParameterProvider<BetDetail> {
odds = 2.0f odds = 2.0f
) )
) )
is BinaryBet -> listOf( is BinaryBet -> listOf(
BetAnswerDetail( BetAnswerDetail(
response = YES_VALUE, response = YES_VALUE,
@ -84,12 +86,14 @@ class BetDetailPreviewProvider : PreviewParameterProvider<BetDetail> {
participations = listOf( participations = listOf(
Participation( Participation(
betId = it.id, betId = it.id,
id = "1",
username = "User1", username = "User1",
response = answers.first().response, response = answers.first().response,
stake = 100 stake = 100
), ),
Participation( Participation(
betId = it.id, betId = it.id,
id = "2",
username = "User 2", username = "User 2",
response = answers.last().response, response = answers.last().response,
stake = 150 stake = 150
@ -97,12 +101,14 @@ class BetDetailPreviewProvider : PreviewParameterProvider<BetDetail> {
), ),
userParticipation = Participation( userParticipation = Participation(
betId = it.id, betId = it.id,
id = "1",
username = "User1", username = "User1",
response = answers.first().response, response = answers.first().response,
stake = 100 stake = 100
), ),
wonParticipation = Participation( wonParticipation = Participation(
betId = it.id, betId = it.id,
id = "1",
username = "User1", username = "User1",
response = answers.first().response, response = answers.first().response,
stake = 100 stake = 100

@ -1,27 +1,64 @@
package fr.iut.alldev.allin.ui.profile package fr.iut.alldev.allin.ui.profile
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityOptionsCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import fr.iut.alldev.allin.ui.profile.components.ProfileScreenHeader import fr.iut.alldev.allin.ui.profile.components.ProfileScreenContent
import fr.iut.alldev.allin.utils.AskPicture
import fr.iut.alldev.allin.utils.AskPictureParams
import fr.iut.alldev.allin.utils.AskPictureResult
import fr.iut.alldev.allin.utils.createImageFile
import java.io.ByteArrayOutputStream
import java.io.File
@Composable @Composable
fun ProfileScreen( fun ProfileScreen(
viewModel: ProfileViewModel = hiltViewModel() viewModel: ProfileViewModel = hiltViewModel()
) { ) {
val state by viewModel.state.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle()
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(contract = AskPicture()) { result ->
when (result) {
is AskPictureResult.PictureResult -> {
val resolver = context.contentResolver
val inputStream = resolver.openInputStream(result.pickedFile!!)
val file = File.createTempFile("image", null, context.cacheDir)
file.outputStream().use { outputStream ->
inputStream?.copyTo(outputStream)
}
val baos = ByteArrayOutputStream()
BitmapFactory.decodeFile(file.path).compress(Bitmap.CompressFormat.JPEG, 100, baos)
val encodedImage = Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT)
viewModel.selectNewProfilePicture(encodedImage)
}
null -> Unit
}
}
Column { Column {
when (val s = state) { when (val s = state) {
is ProfileViewModel.State.Loaded -> { is ProfileViewModel.State.Loaded -> {
ProfileScreenHeader( ProfileScreenContent(
image = null, image = s.user.image,
username = s.user.username, username = s.user.username,
totalBets = 333, totalBets = s.user.nbBets,
bestWin = 365, bestWin = s.user.bestWin,
friends = 3 friends = s.user.nbFriends,
selectNewProfilePicture = {
val file = createImageFile(context = context)
launcher.launch(AskPictureParams(file), ActivityOptionsCompat.makeBasic())
}
) )
} }

@ -6,17 +6,21 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.data.model.User
import fr.iut.alldev.allin.data.repository.UserRepository import fr.iut.alldev.allin.data.repository.UserRepository
import fr.iut.alldev.allin.keystore.AllInKeystoreManager
import fr.iut.alldev.allin.ui.navigation.Arguments import fr.iut.alldev.allin.ui.navigation.Arguments
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ProfileViewModel @Inject constructor( class ProfileViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val userRepository: UserRepository private val userRepository: UserRepository,
private val keystoreManager: AllInKeystoreManager
) : ViewModel() { ) : ViewModel() {
private val userId: String? = savedStateHandle[Arguments.USER_ID] private val userId: String? = savedStateHandle[Arguments.USER_ID]
@ -28,15 +32,34 @@ class ProfileViewModel @Inject constructor(
userRepository.currentUserState.value?.let { currentUser -> userRepository.currentUserState.value?.let { currentUser ->
if (userId == currentUser.id) { if (userId == currentUser.id) {
_state.emit(State.Loaded(currentUser)) userRepository.currentUserState.filterNotNull().collect { user ->
_state.emit(State.Loaded(user = user, isCurrentUser = true))
}
} }
} }
} }
} }
fun selectNewProfilePicture(base64: String) {
viewModelScope.launch {
try {
userRepository.setImage(
token = keystoreManager.getTokenOrEmpty(),
base64 = base64
)
userRepository.currentUserState.value?.let {
userRepository.updateCurrentUserImage(value = userRepository.getImageUrl(it.id))
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
sealed interface State { sealed interface State {
data object Loading : State data object Loading : State
data class Loaded(val user: User) : State data class Loaded(val user: User, val isCurrentUser: Boolean) : State
} }
} }

@ -0,0 +1,52 @@
package fr.iut.alldev.allin.ui.profile.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.iut.alldev.allin.theme.AllInTheme
@Composable
fun ProfileScreenContent(
username: String,
image: String?,
totalBets: Int,
bestWin: Int,
friends: Int,
selectNewProfilePicture: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 18.dp, horizontal = 21.dp)
) {
ProfileScreenHeader(
image = image,
username = username,
totalBets = totalBets,
bestWin = bestWin,
friends = friends,
selectNewProfilePicture = selectNewProfilePicture
)
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ProfileScreenContentPreview() {
AllInTheme {
ProfileScreenContent(
username = "User 1",
image = null,
totalBets = 12,
bestWin = 365,
friends = 5,
selectNewProfilePicture = { }
)
}
}

@ -1,12 +1,15 @@
package fr.iut.alldev.allin.ui.profile.components package fr.iut.alldev.allin.ui.profile.components
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.res.stringResource import androidx.compose.ui.res.stringResource
@ -24,8 +27,11 @@ fun ProfileScreenHeader(
image: String?, image: String?,
totalBets: Int, totalBets: Int,
bestWin: Int, bestWin: Int,
friends: Int friends: Int,
selectNewProfilePicture: () -> Unit
) { ) {
val interactionSource = remember { MutableInteractionSource() }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
@ -33,7 +39,12 @@ fun ProfileScreenHeader(
ProfilePicture( ProfilePicture(
image = image, image = image,
fallback = username.asFallbackProfileUsername(), fallback = username.asFallbackProfileUsername(),
size = 68.dp size = 68.dp,
modifier = Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = selectNewProfilePicture
)
) )
Column( Column(
verticalArrangement = Arrangement.SpaceEvenly, verticalArrangement = Arrangement.SpaceEvenly,
@ -102,7 +113,8 @@ private fun ProfileScreenHeaderPreview() {
image = null, image = null,
totalBets = 12, totalBets = 12,
bestWin = 365, bestWin = 365,
friends = 5 friends = 5,
selectNewProfilePicture = { }
) )
} }
} }

@ -0,0 +1,59 @@
package fr.iut.alldev.allin.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.FileProvider
import fr.iut.alldev.allin.R
import java.io.File
class AskPicture : ActivityResultContract<AskPictureParams, AskPictureResult?>() {
private var uri: Uri? = null
override fun createIntent(context: Context, input: AskPictureParams): Intent {
val pickerIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.also { intent ->
input.cameraFile.also { file ->
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
).also { uri = it }
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
}
}
return Intent.createChooser(cameraIntent, context.getText(R.string.profile_pick_profile_picture))
.also { intent ->
intent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(pickerIntent))
}
}
override fun parseResult(resultCode: Int, intent: Intent?): AskPictureResult? {
return if (resultCode != Activity.RESULT_OK) {
null
} else {
(intent?.data ?: uri)?.let {
AskPictureResult.PictureResult(it)
}
}
}
}
data class AskPictureParams(val cameraFile: File)
sealed class AskPictureResult {
data class PictureResult(val pickedFile: Uri?) : AskPictureResult()
}
fun createImageFile(context: Context): File {
val directory = File(context.cacheDir, ".").apply {
mkdirs()
}
return File.createTempFile("picture_", ".jpg", directory)
}

@ -187,4 +187,7 @@
<string name="daily_reward_title">Récompense quotidienne</string> <string name="daily_reward_title">Récompense quotidienne</string>
<string name="daily_reward_subtitle">Votre récompense quotidienne est débloquée tous les jours à 00:00 UTC et vous permets dobtenir entre 10 et 150 Allcoins.</string> <string name="daily_reward_subtitle">Votre récompense quotidienne est débloquée tous les jours à 00:00 UTC et vous permets dobtenir entre 10 et 150 Allcoins.</string>
<!--Profile-->
<string name="profile_pick_profile_picture">Choisissez une nouvelle image de profil</string>
</resources> </resources>

@ -184,4 +184,7 @@
<string name="daily_reward_title">Daily reward</string> <string name="daily_reward_title">Daily reward</string>
<string name="daily_reward_subtitle">Your daily reward is unlocked every day at 00:00 UTC and allows you to get between 10 and 150 Allcoins.</string> <string name="daily_reward_subtitle">Your daily reward is unlocked every day at 00:00 UTC and allows you to get between 10 and 150 Allcoins.</string>
<!--Profile-->
<string name="profile_pick_profile_picture">Pick a new profile picture</string>
</resources> </resources>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="allin"
path="." />
</paths>

@ -42,6 +42,12 @@ interface AllInApi {
@GET("users/gift") @GET("users/gift")
suspend fun dailyGift(@Header("Authorization") token: String): Int suspend fun dailyGift(@Header("Authorization") token: String): Int
@POST("users/images")
suspend fun setImage(
@Header("Authorization") token: String,
@Body base64: RequestBody
)
// FRIENDS // FRIENDS
// --------------------- // ---------------------

@ -154,6 +154,10 @@ class MockAllInApi : AllInApi {
} else throw MockAllInApiException("Gift already taken today") } else throw MockAllInApiException("Gift already taken today")
} }
override suspend fun setImage(token: String, base64: RequestBody) {
val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
}
override suspend fun getFriends(token: String): List<ResponseUser> { override suspend fun getFriends(token: String): List<ResponseUser> {
val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.") val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.")
return mockFriends return mockFriends

@ -16,6 +16,7 @@ data class ResponseParticipation(
fun toParticipation() = fun toParticipation() =
Participation( Participation(
betId = betId, betId = betId,
id = id,
username = username, username = username,
response = answer, response = answer,
stake = stake stake = stake

@ -4,6 +4,7 @@ import fr.iut.alldev.allin.data.api.model.RequestParticipation
data class Participation( data class Participation(
val betId: String, val betId: String,
val id: String,
val username: String, val username: String,
val response: String, val response: String,
val stake: Int val stake: Int

@ -23,8 +23,25 @@ abstract class UserRepository {
} }
} }
suspend fun updateCurrentUserImage(value: String?) {
currentUserState.value?.let { user ->
currentUser.emit(
user.copy(
image = null
)
)
currentUser.emit(
user.copy(
image = value
)
)
}
}
abstract suspend fun login(username: String, password: String): String? abstract suspend fun login(username: String, password: String): String?
abstract suspend fun login(token: String): String? abstract suspend fun login(token: String): String?
abstract suspend fun register(username: String, email: String, password: String): String? abstract suspend fun register(username: String, email: String, password: String): String?
abstract suspend fun dailyGift(token: String): Int abstract suspend fun dailyGift(token: String): Int
abstract suspend fun setImage(token: String, base64: String)
abstract fun getImageUrl(id: String): String
} }

@ -1,16 +1,22 @@
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.CheckUser import fr.iut.alldev.allin.data.api.model.CheckUser
import fr.iut.alldev.allin.data.api.model.RequestUser import fr.iut.alldev.allin.data.api.model.RequestUser
import fr.iut.alldev.allin.data.di.AllInUrl
import fr.iut.alldev.allin.data.repository.UserRepository import fr.iut.alldev.allin.data.repository.UserRepository
import okhttp3.HttpUrl
import javax.inject.Inject import javax.inject.Inject
class UserRepositoryImpl @Inject constructor( class UserRepositoryImpl @Inject constructor(
private val api: AllInApi private val api: AllInApi,
@AllInUrl private val apiUrl: HttpUrl
) : UserRepository() { ) : UserRepository() {
override fun getImageUrl(id: String) = "$apiUrl/users/images/$id"
override suspend fun login(username: String, password: String): String? { override suspend fun login(username: String, password: String): String? {
val response = api.login( val response = api.login(
CheckUser( CheckUser(
@ -41,5 +47,9 @@ class UserRepositoryImpl @Inject constructor(
} }
override suspend fun dailyGift(token: String): Int = override suspend fun dailyGift(token: String): Int =
api.dailyGift(token) api.dailyGift(token.formatBearerToken())
override suspend fun setImage(token: String, base64: String) {
api.setImage(token = token.formatBearerToken(), base64 = base64.asRequestBody())
}
} }
Loading…
Cancel
Save