diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69a5f61..bd87c32 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,7 @@ if (secretsPropertiesFile.exists()) { plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) version "1.9.10" - alias(libs.plugins.apollo) + id("com.apollographql.apollo") version "4.0.0" id("kotlin-kapt") id("com.google.dagger.hilt.android") id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" // this version matches your Kotlin version @@ -67,6 +67,9 @@ android { } kotlinOptions { jvmTarget = "1.8" + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" + ) } } @@ -97,12 +100,13 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation(libs.apollo.runtime) + implementation("com.apollographql.apollo:apollo-runtime:4.0.0") implementation("io.coil-kt.coil3:coil-compose:3.1.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0") lintChecks(libs.compose.lint.checks) implementation(platform("com.google.firebase:firebase-bom:34.3.0")) implementation("com.google.firebase:firebase-analytics") + implementation("androidx.compose.material:material-icons-extended") } apollo { @@ -114,4 +118,3 @@ apollo { } } } - diff --git a/app/src/main/graphql/Games.graphql b/app/src/main/graphql/Games.graphql index 0af8c70..66fe3c6 100644 --- a/app/src/main/graphql/Games.graphql +++ b/app/src/main/graphql/Games.graphql @@ -2,6 +2,7 @@ query Games{ games{ id date + time city sport team{ diff --git a/app/src/main/java/com/cornellappdev/score/components/ProfileGameCarousel.kt b/app/src/main/java/com/cornellappdev/score/components/ProfileGameCarousel.kt new file mode 100644 index 0000000..6167402 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/ProfileGameCarousel.kt @@ -0,0 +1,96 @@ +package com.cornellappdev.score.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChevronRight +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.res.painterResource +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R +import com.cornellappdev.score.model.GameCardData +import com.cornellappdev.score.theme.CornellRed +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.Style + +@Composable +fun ProfileGameCarousel( + title: String, + games: List, + onClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = Style.heading6 + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "${games.size} Results", + style = Style.bodyNormalGray + ) + Icon( + imageVector = Icons.Outlined.ChevronRight, + contentDescription = null, + tint = GrayMedium, + modifier = Modifier.size(18.dp) + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + LazyRow( + contentPadding = PaddingValues(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(games) { game -> + FeaturedGameCard( + leftTeamLogo = painterResource(R.drawable.cornell_logo), + rightTeamLogo = game.teamLogo, + team = game.team, + date = game.dateString, + isLive = game.isLive, + isPast = game.isPast, + genderIcon = painterResource(game.genderIcon), + sportIcon = painterResource(game.sportIcon), + location = game.location, + gradientColor1 = CornellRed, + gradientColor2 = game.teamColor, + leftScore = game.cornellScore?.toInt(), + rightScore = game.otherScore?.toInt(), + onClick = { onClick(game.id) }, + modifier = Modifier.width(241.dp), + headerModifier = Modifier + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index d6c93d7..6074d76 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -20,6 +20,7 @@ data class Game( val id: String, val teamName: String, val teamLogo: String, + val time: String?, val teamColor: Color, val gender: String, val sport: String, @@ -146,7 +147,7 @@ data class TeamScore( // Aggregated game data showing scores for both teams data class GameData( val teamScores: Pair -){ +) { val maxPeriods: Int get() = maxOf( @@ -250,7 +251,11 @@ fun Game.toGameCardData(): GameCardData { date = parseDateOrNull(date), dateString = parseDateOrNull(date)?.format(outputFormatter) ?: date, - isLive = (LocalDate.now() == parseDateOrNull(date)), + isLive = parseDateTimeOrNull(date, time ?: "")?.let { startTime -> + val now = LocalDateTime.now() + val endTime = startTime.plusHours(2) + now.isAfter(startTime) && now.isBefore(endTime) + } ?: false, isPast = isPast, location = city, gender = gender, @@ -290,14 +295,15 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData { scoreBreakdown = scoreBreakdown, team1 = TeamBoxScore("Cornell"), team2 = TeamBoxScore(team?.name ?: ""), - sport = sport + sport = sport, + result = result ?: "" ), scoreEvent = boxScore?.toScoreEvents(team?.image ?: "") ?: emptyList(), daysUntilGame = daysUntil, hoursUntilGame = hoursUntil, - homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport).second + homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport, result ?: "").second ?: parsedScores?.first ?: 0, - oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport).second + oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport, result ?: "").second ?: parsedScores?.second ?: 0 ) } diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 2b84524..1e9d91b 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -4,6 +4,7 @@ import android.util.Log import com.apollographql.apollo.ApolloClient import com.cornellappdev.score.util.isValidSport import com.cornellappdev.score.util.parseColor +import com.cornellappdev.score.util.parseResultScore import com.example.score.GameByIdQuery import com.example.score.GamesQuery import com.example.score.PagedGamesQuery @@ -75,6 +76,7 @@ class ScoreRepository @Inject constructor( id = game.id ?: "", // Should never be null teamLogo = it, teamName = game.team.name, + time = game.time, teamColor = parseColor(game.team.color).copy(alpha = 0.4f * 255), gender = if (game.gender == "Mens") "Men's" else "Women's", sport = game.sport, @@ -133,11 +135,15 @@ class ScoreRepository @Inject constructor( .mapNotNull { graphqlGame -> val scores = graphqlGame.result?.split(",")?.getOrNull(1)?.split("-") val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() - val otherScore = scores?.getOrNull(1)?.toNumberOrNull() + ?: parseResultScore(graphqlGame.result)?.first + val otherScore = scores?.getOrNull(1)?.toNumberOrNull() ?: parseResultScore( + graphqlGame.result + )?.second graphqlGame.team?.image?.let { imageUrl -> Game( id = graphqlGame.id ?: "", teamLogo = imageUrl, + time = graphqlGame.time, teamName = graphqlGame.team.name, teamColor = parseColor(graphqlGame.team.color).copy(alpha = 0.4f * 255), gender = if (graphqlGame.gender == "Mens") "Men's" else "Women's", @@ -171,6 +177,7 @@ class ScoreRepository @Inject constructor( * `currentGamesFlow` to be observed. */ fun getGameById(id: String) = appScope.launch { + Log.d("ScoreRepository", "Fetching game with id: $id") _currentGameFlow.value = ApiResponse.Loading try { val result = @@ -181,6 +188,7 @@ class ScoreRepository @Inject constructor( result.getOrNull()?.game?.let { _currentGameFlow.value = ApiResponse.Success(it.toGameDetails()) + } ?: _currentGameFlow.update { ApiResponse.Error } } catch (e: Exception) { Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e) diff --git a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt index ba66aa0..43e2f28 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavHost.kt @@ -11,11 +11,13 @@ import androidx.navigation.toRoute import com.cornellappdev.score.model.ScoreEvent import com.cornellappdev.score.nav.root.ScoreScreens import com.cornellappdev.score.nav.root.ScoreScreens.Home +import com.cornellappdev.score.screen.EditProfileScreen import com.cornellappdev.score.screen.GameDetailsScreen import com.cornellappdev.score.screen.HighlightsScreen import com.cornellappdev.score.screen.HighlightsSearchScreen import com.cornellappdev.score.screen.HomeScreen import com.cornellappdev.score.screen.PastGamesScreen +import com.cornellappdev.score.screen.ProfileScreen import com.cornellappdev.score.util.highlightsList import com.cornellappdev.score.util.recentSearchList import com.cornellappdev.score.util.sportList @@ -35,9 +37,14 @@ fun ScoreNavHost(navController: NavHostController) { ) { composable { CompositionLocalProvider(LocalViewModelStoreOwner provides mainScreenViewModelStoreOwner) { - HomeScreen(navigateToGameDetails = { - navController.navigate(ScoreScreens.GameDetailsPage(it)) - }) + HomeScreen( + navigateToGameDetails = { + navController.navigate(ScoreScreens.GameDetailsPage(it)) + }, + navigateToProfile = { + navController.navigate(ScoreScreens.Profile) + } + ) } } composable { @@ -47,8 +54,27 @@ fun ScoreNavHost(navController: NavHostController) { }) } } - composable { + composable { + ProfileScreen( + navigateToEditProfile = { + navController.navigate(ScoreScreens.EditProfile) + }, + navigateToGameDetails = { gameId -> + navController.navigate(ScoreScreens.GameDetailsPage(gameId)) + } + ) + } + composable { + EditProfileScreen( + onBackClick = { + navController.navigateUp() + } + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() GameDetailsScreen( + gameId = route.gameId, onBackArrow = { navController.navigateUp() }, @@ -89,4 +115,3 @@ fun ScoreNavHost(navController: NavHostController) { // } } } - diff --git a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavigationBar.kt b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavigationBar.kt index 91af73d..bc9374a 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/ScoreNavigationBar.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/ScoreNavigationBar.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavBackStackEntry @@ -38,8 +39,10 @@ fun ScoreNavigationBar( selectedIndicatorColor = Color.Transparent ), icon = { + val icon = if (isSelected) item.selectedIcon else item.unselectedIcon + Icon( - painter = painterResource(id = if (isSelected) item.selectedIcon else item.unselectedIcon), + painter = painterResource(id = icon), contentDescription = null, tint = Color.Unspecified ) diff --git a/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt b/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt index 8542ff7..81909eb 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt @@ -9,6 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -18,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -65,7 +70,8 @@ fun RootNavigation( Scaffold( modifier = modifier.fillMaxSize(), bottomBar = { - if (navBackStackEntry?.toScreen() is ScoreScreens.GameDetailsPage) { + val currentScreen = navBackStackEntry?.toScreen() + if (currentScreen is ScoreScreens.GameDetailsPage || currentScreen is ScoreScreens.EditProfile) { return@Scaffold } Surface( @@ -110,6 +116,12 @@ sealed class ScoreScreens { @Serializable data class GameScoreSummaryPage(val scoreEvents: String) : ScoreScreens() + @Serializable + data object Profile : ScoreScreens() + + @Serializable + data object EditProfile : ScoreScreens() + ////removed for 2/2026 release // @Serializable // data object HighlightsScreen : ScoreScreens() @@ -124,10 +136,12 @@ fun NavBackStackEntry.toScreen(): ScoreScreens? = "GameDetailsPage" -> toRoute() "ScoresScreen" -> toRoute() "GameScoreSummaryPage" -> toRoute() + "Profile" -> toRoute() + "EditProfile" -> toRoute() //removed for 2/2026 release // "HighlightsScreen" -> toRoute() // "HighlightsSearchScreen" -> toRoute() - else -> throw IllegalArgumentException("Invalid screen") + else -> null } data class NavItem( @@ -157,4 +171,4 @@ val tabs = listOf( selectedIcon = R.drawable.ic_scores_filled, screen = ScoreScreens.ScoresScreen, ), -) \ No newline at end of file +) diff --git a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt new file mode 100644 index 0000000..5fe691b --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt @@ -0,0 +1,408 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +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 com.cornellappdev.score.R +import com.cornellappdev.score.theme.BorderDark +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.Wash +import com.cornellappdev.score.theme.White + +@Composable +fun EditProfileScreen(onBackClick: () -> Unit) { + var name by remember { mutableStateOf("Audrey Wu") } + var username by remember { mutableStateOf("audreywuu") } + var currentAvatar by remember { + mutableStateOf(AvatarOption(R.drawable.pingpong_profile)) + } + var showAvatarPicker by remember { mutableStateOf(false) } + + Box(modifier = Modifier.fillMaxSize()) { + EditProfileContent( + name = name, + onNameChange = { name = it }, + username = username, + onUsernameChange = { username = it }, + currentAvatar = currentAvatar, + onBackClick = onBackClick, + onEditPhotoClick = { showAvatarPicker = true }, + onSaveClick = { /* Handle save */ } + ) + + if (showAvatarPicker) { + AvatarPickerBottomSheet( + onDismiss = { showAvatarPicker = false }, + onAvatarSelected = { + currentAvatar = it + showAvatarPicker = false + } + ) + } + } +} + +@Composable +fun EditProfileContent( + name: String, + onNameChange: (String) -> Unit, + username: String, + onUsernameChange: (String) -> Unit, + currentAvatar: AvatarOption, + onBackClick: () -> Unit, + onEditPhotoClick: () -> Unit, + onSaveClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(White) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 4.dp) + ) { + IconButton( + onClick = onBackClick, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + painter = painterResource(id = R.drawable.back_arrow), + contentDescription = "Back", + modifier = Modifier.size(24.dp) + ) + } + + Text( + text = "Edit profile", + style = Style.heading2.copy( + fontSize = 18.sp, + color = GrayPrimary + ), + modifier = Modifier.align(Alignment.Center) + ) + } + + HorizontalDivider(color = Wash) + Spacer(modifier = Modifier.height(24.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(120.dp) + .border(width = 0.81.dp, color = BorderDark, shape = CircleShape) + .clip(CircleShape) + .background(White), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = currentAvatar.imageRes), + contentDescription = "Profile photo", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Edit photo", + style = Style.heading6.copy(color = CrimsonPrimary), + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { onEditPhotoClick() } + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Name", + style = Style.heading6 + ) + + BasicTextField( + value = name, + onValueChange = onNameChange, + textStyle = Style.heading5.copy( + color = GrayPrimary, + textAlign = TextAlign.End + ), + modifier = Modifier.weight(1f) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Username", + style = Style.heading6 + ) + + BasicTextField( + value = username, + onValueChange = onUsernameChange, + textStyle = Style.heading5.copy( + color = GrayPrimary, + textAlign = TextAlign.End + ), + modifier = Modifier.weight(1f) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = onSaveClick, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 60.dp) + .width(138.dp) + .height(46.dp), + shape = RoundedCornerShape(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + disabledContainerColor = GrayLight + ), + contentPadding = PaddingValues(0.dp) + ) { + Text( + text = "Save", + style = Style.title.copy( + color = White, + textAlign = TextAlign.Center + ), + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AvatarPickerBottomSheet( + onDismiss: () -> Unit, + onAvatarSelected: (AvatarOption) -> Unit +) { + var selectedAvatar by remember { mutableStateOf(null) } + + val avatars = listOf( + AvatarOption(R.drawable.upload), + AvatarOption(R.drawable.pingpong_profile), + AvatarOption(R.drawable.soccer_profile), + AvatarOption(R.drawable.tennis_profile), + AvatarOption(R.drawable.badminton_profile), + AvatarOption(R.drawable.billiards_profile), + AvatarOption(R.drawable.basketball_profile), + AvatarOption(R.drawable.bowling_profile), + AvatarOption(R.drawable.volleyball_profile), + AvatarOption(R.drawable.golf_profile), + AvatarOption(R.drawable.weightlifting_profile), + AvatarOption(R.drawable.baseball_profile) + ) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = White, + dragHandle = null + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Box( + modifier = Modifier + .size(32.dp) + .border(1.dp, GrayLight, CircleShape) + .clip(CircleShape) + .clickable { onDismiss() }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + modifier = Modifier.size(16.dp), + tint = GrayMedium + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + AvatarIconGrid( + avatars = avatars, + selectedAvatar = selectedAvatar, + onAvatarSelected = { selectedAvatar = it } + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + selectedAvatar?.let(onAvatarSelected) + }, + enabled = selectedAvatar != null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp) + .width(138.dp) + .height(46.dp), + shape = RoundedCornerShape(50.dp), + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + disabledContainerColor = GrayLight + ), + contentPadding = PaddingValues(0.dp) + ) { + Text( + text = "Save", + style = Style.title.copy( + color = White, + textAlign = TextAlign.Center + ), + modifier = Modifier.fillMaxWidth() + ) + } + + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Composable +fun AvatarIconGrid( + avatars: List, + selectedAvatar: AvatarOption?, + onAvatarSelected: (AvatarOption) -> Unit +) { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(avatars) { avatar -> + val isSelected = selectedAvatar == avatar + + Box( + modifier = Modifier + .aspectRatio(1f) + .clip(CircleShape) + .clickable { onAvatarSelected(avatar) }, + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = avatar.imageRes), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + if (isSelected) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = White.copy(alpha = 0.45f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + modifier = Modifier.size(40.dp), + tint = GrayPrimary + ) + } + } + } + } + } +} +// TODO: support uploaded profile photos later +data class AvatarOption( + val imageRes: Int +) +@Preview(showBackground = true) +@Composable +private fun EditProfileScreenPreview() { + MaterialTheme { + EditProfileScreen(onBackClick = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt index 001f500..9af8246 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt @@ -58,6 +58,7 @@ import java.time.LocalDate @Composable fun GameDetailsScreen( + gameId: String, gameDetailsViewModel: GameDetailsViewModel = hiltViewModel(), onBackArrow: () -> Unit = {}, navigateToGameScoreSummary: (List) -> Unit diff --git a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt index 1536efa..6485962 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -53,12 +53,19 @@ import com.cornellappdev.score.util.gameList import com.cornellappdev.score.util.sportSelectionList import com.cornellappdev.score.viewmodel.HomeUiState import com.cornellappdev.score.viewmodel.HomeViewModel +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.size +import androidx.compose.ui.draw.clip +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel(), - navigateToGameDetails: (String) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {}, + navigateToProfile: () -> Unit = {} ) { val uiState = homeViewModel.collectUiStateValue() var showBottomSheet by remember { mutableStateOf(false) } @@ -83,8 +90,9 @@ fun HomeScreen( onGenderSelected = { homeViewModel.onGenderSelected(it) }, onSportSelected = { homeViewModel.onSportSelected(it) }, navigateToGameDetails = navigateToGameDetails, + navigateToProfile = navigateToProfile, onRefresh = { homeViewModel.onRefresh() }, - onAdvancedFilterClick = { showBottomSheet = true } + onAdvancedFilterClick = { showBottomSheet = true }, ) } } @@ -112,6 +120,7 @@ private fun HomeContent( onSportSelected: (SportSelection) -> Unit, onRefresh: () -> Unit, navigateToGameDetails: (String) -> Unit = {}, + navigateToProfile: () -> Unit = {}, onAdvancedFilterClick: () -> Unit ) { ScorePullToRefreshBox(isRefreshing = uiState.loadedState == ApiResponse.Loading, onRefresh) { @@ -120,6 +129,7 @@ private fun HomeContent( onGenderSelected, onSportSelected, navigateToGameDetails, + navigateToProfile, onAdvancedFilterClick ) } @@ -132,23 +142,37 @@ private fun HomeLazyColumn( onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, navigateToGameDetails: (String) -> Unit, + navigateToProfile: () -> Unit, onAdvancedFilterClick: () -> Unit ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { - if (uiState.filteredGames.isNotEmpty()) { - item { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { Text( text = "Upcoming", style = heading1, - color = GrayPrimary, + color = GrayPrimary + ) + + Image( + painter = painterResource(id = R.drawable.profile_ic), + contentDescription = "Profile", modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp) + .size(28.dp) + .clip(CircleShape) + .clickable { navigateToProfile() } ) } - item { - Spacer(Modifier.height(16.dp)) - } + } + + item { + Spacer(Modifier.height(16.dp)) } if (uiState.filteredGames.isNotEmpty()) { item { @@ -242,6 +266,7 @@ private fun HomeScreenPreview() = ScorePreview { onGenderSelected = {}, onSportSelected = {}, onRefresh = {}, + navigateToProfile = {}, onAdvancedFilterClick = {} ) } @@ -260,6 +285,7 @@ private fun HomeScreenEmptyStatePreview() = ScorePreview { onGenderSelected = {}, onSportSelected = {}, onRefresh = {}, + navigateToProfile = {}, onAdvancedFilterClick = {} ) } diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfileScreen.kt new file mode 100644 index 0000000..20d314b --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfileScreen.kt @@ -0,0 +1,193 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.components.ProfileGameCarousel +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White +import com.cornellappdev.score.util.gameList + +@Composable +fun ProfileScreen( + modifier: Modifier = Modifier, + navigateToEditProfile: () -> Unit = {}, + navigateToGameDetails: (String) -> Unit = {} +) { + val bookmarkedGames = gameList.take(4) + val recommendedGames = gameList.takeLast(4) + + Column( + modifier = modifier + .fillMaxSize() + .background(White) + ) { + ProfileTopBar( + modifier = Modifier.padding(horizontal = 20.dp) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + ProfileRow( + modifier = Modifier.padding(horizontal = 20.dp), + onEditClick = navigateToEditProfile + ) + + Spacer(modifier = Modifier.height(24.dp)) + + ProfileGameCarousel( + title = "Bookmarks", + games = bookmarkedGames, + onClick = { gameId -> navigateToGameDetails(gameId) } + ) + + ProfileGameCarousel( + title = "Games You Might Like", + games = recommendedGames, + onClick = { gameId -> navigateToGameDetails(gameId) } + ) + } +} + +@Composable +fun ProfileTopBar(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Profile", + style = Style.title.copy( + fontSize = 26.sp, + color = GrayPrimary + ) + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Notifications, + contentDescription = "Notifications", + tint = GrayPrimary, + modifier = Modifier.size(24.dp) + ) + + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Menu", + tint = GrayPrimary, + modifier = Modifier.size(24.dp) + ) + } + } +} + +@Composable +fun ProfileRow( + modifier: Modifier = Modifier, + onEditClick: () -> Unit = {} +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.pingpong_profile), + contentDescription = "Profile image", + modifier = Modifier.size(100.dp), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(24.dp)) + + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "Audrey Wu", + style = Style.heading6 + ) + Text( + text = "@audreywuu", + style = Style.heading5.copy( + color = GrayPrimary + ) + ) + Spacer(modifier = Modifier.height(6.dp)) + EditProfileButton(onClick = onEditClick) + } + } +} + +@Composable +fun EditProfileButton( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + OutlinedButton( + onClick = onClick, + shape = RoundedCornerShape(100.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = White, + contentColor = GrayPrimary + ), + border = BorderStroke(1.dp, GrayLight), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + modifier = modifier + .width(97.dp) + .height(32.dp) + ) { + Text( + text = "Edit profile", + style = Style.labelsMedium.copy( + color = GrayPrimary + ) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ProfileScreenPreview() { + MaterialTheme { + ProfileScreen() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/theme/Color.kt b/app/src/main/java/com/cornellappdev/score/theme/Color.kt index a4f8fa5..db5d7a5 100644 --- a/app/src/main/java/com/cornellappdev/score/theme/Color.kt +++ b/app/src/main/java/com/cornellappdev/score/theme/Color.kt @@ -18,6 +18,7 @@ val DarkBlue = Color(0x011f5b) val saturatedGreen = Color(0xFF55B63C) val SpotColor = Color(0x12000000) val AmbientColor = Color(0x12000000) +val BorderDark = Color(0xFF1D353E) //placeholders, will be replaced once we get backend data val CornellRed = Color(0x66B31B1B) diff --git a/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt b/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt index a25e9ee..2da4dbb 100644 --- a/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt +++ b/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt @@ -1,5 +1,6 @@ package com.cornellappdev.score.util +import android.util.Log import com.cornellappdev.score.model.GameData import com.cornellappdev.score.model.TeamBoxScore import com.cornellappdev.score.model.TeamScore @@ -17,7 +18,7 @@ import com.cornellappdev.score.model.TeamScore * @return a pair where the first value is a list of parsed period scores and the second is the total score (or null if invalid) */ // TODO: ASK ABOUT OT. Other sports might be added. -fun convertScores(scoreList: List?, sport: String): Pair, Int?> { +fun convertScores(scoreList: List?, sport: String, result: String): Pair, Int?> { if (scoreList == null || scoreList.size < 2) return Pair(emptyList(), null) var scoresByPeriod = scoreList @@ -31,7 +32,12 @@ fun convertScores(scoreList: List?, sport: String): Pair, Int } if (sport.lowercase() == "baseball") { - scoresByPeriod = scoresByPeriod.take(9) + val scoreParsed = result.split("(") + scoresByPeriod = if (scoreParsed.size > 1) { + scoresByPeriod.take(6) + } else { + scoresByPeriod.take(9) + } val totalScore = scoresByPeriod.sum() return Pair(scoresByPeriod, totalScore) } @@ -56,14 +62,15 @@ fun toGameData( scoreBreakdown: List?>?, team1: TeamBoxScore, team2: TeamBoxScore, - sport: String + sport: String, + result: String, ): GameData { val (team1Scores, team1Total) = scoreBreakdown?.getOrNull(0)?.let { - convertScores(it, sport) + convertScores(it, sport, result) } ?: (emptyList() to null) val (team2Scores, team2Total) = scoreBreakdown?.getOrNull(1)?.let { - convertScores(it, sport) + convertScores(it, sport, result) } ?: (emptyList() to null) val team1Score = @@ -90,11 +97,12 @@ fun parseResultScore(result: String?): Pair? { if (parts.size != 2) return null val scorePart = parts[1].split("-") + val secondScorePartEdge = scorePart[1].split("(") if (scorePart.size != 2) return null val homeScore = scorePart[0].toIntOrNull() - val oppScore = scorePart[1].toIntOrNull() - + val oppScore = secondScorePartEdge[0].toIntOrNull() + Log.d("HIHI", oppScore.toString()) if (homeScore != null && oppScore != null) { return Pair(homeScore, oppScore) } else { diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt index 1b69f30..669ba10 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt @@ -24,7 +24,7 @@ class GameDetailsViewModel @Inject constructor( loadedState = ApiResponse.Loading ) ) { - private val gameId: String = checkNotNull(savedStateHandle["gameId"]) + private val gameId: String = savedStateHandle.toRoute().gameId init { asyncCollect(scoreRepository.currentGamesFlow) { response -> @@ -43,4 +43,4 @@ class GameDetailsViewModel @Inject constructor( applyMutation { copy(loadedState = ApiResponse.Loading) } scoreRepository.getGameById(gameId) } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/back_arrow.xml b/app/src/main/res/drawable/back_arrow.xml new file mode 100644 index 0000000..a30295f --- /dev/null +++ b/app/src/main/res/drawable/back_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/badminton_profile.png b/app/src/main/res/drawable/badminton_profile.png new file mode 100644 index 0000000..3b1229f Binary files /dev/null and b/app/src/main/res/drawable/badminton_profile.png differ diff --git a/app/src/main/res/drawable/baseball_profile.png b/app/src/main/res/drawable/baseball_profile.png new file mode 100644 index 0000000..2de0c5d Binary files /dev/null and b/app/src/main/res/drawable/baseball_profile.png differ diff --git a/app/src/main/res/drawable/basketball_profile.png b/app/src/main/res/drawable/basketball_profile.png new file mode 100644 index 0000000..23eda19 Binary files /dev/null and b/app/src/main/res/drawable/basketball_profile.png differ diff --git a/app/src/main/res/drawable/billiards_profile.png b/app/src/main/res/drawable/billiards_profile.png new file mode 100644 index 0000000..b0ae5b8 Binary files /dev/null and b/app/src/main/res/drawable/billiards_profile.png differ diff --git a/app/src/main/res/drawable/bowling_profile.png b/app/src/main/res/drawable/bowling_profile.png new file mode 100644 index 0000000..a6b7770 Binary files /dev/null and b/app/src/main/res/drawable/bowling_profile.png differ diff --git a/app/src/main/res/drawable/football_profile.png b/app/src/main/res/drawable/football_profile.png new file mode 100644 index 0000000..23d8d30 Binary files /dev/null and b/app/src/main/res/drawable/football_profile.png differ diff --git a/app/src/main/res/drawable/golf_profile.png b/app/src/main/res/drawable/golf_profile.png new file mode 100644 index 0000000..3429a7c Binary files /dev/null and b/app/src/main/res/drawable/golf_profile.png differ diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..a8b409b --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 0000000..40c739b --- /dev/null +++ b/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_profile_filled.xml b/app/src/main/res/drawable/ic_profile_filled.xml new file mode 100644 index 0000000..00d13a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_profile_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/pingpong.png b/app/src/main/res/drawable/pingpong.png new file mode 100644 index 0000000..31a283f Binary files /dev/null and b/app/src/main/res/drawable/pingpong.png differ diff --git a/app/src/main/res/drawable/pingpong_profile.png b/app/src/main/res/drawable/pingpong_profile.png new file mode 100644 index 0000000..0a2f0ca Binary files /dev/null and b/app/src/main/res/drawable/pingpong_profile.png differ diff --git a/app/src/main/res/drawable/profile_ic.xml b/app/src/main/res/drawable/profile_ic.xml new file mode 100644 index 0000000..56d022e --- /dev/null +++ b/app/src/main/res/drawable/profile_ic.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/soccer_profile.png b/app/src/main/res/drawable/soccer_profile.png new file mode 100644 index 0000000..94af83f Binary files /dev/null and b/app/src/main/res/drawable/soccer_profile.png differ diff --git a/app/src/main/res/drawable/tennis_profile.png b/app/src/main/res/drawable/tennis_profile.png new file mode 100644 index 0000000..63ef187 Binary files /dev/null and b/app/src/main/res/drawable/tennis_profile.png differ diff --git a/app/src/main/res/drawable/upload.xml b/app/src/main/res/drawable/upload.xml new file mode 100644 index 0000000..c43a7e6 --- /dev/null +++ b/app/src/main/res/drawable/upload.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/volleyball_profile.png b/app/src/main/res/drawable/volleyball_profile.png new file mode 100644 index 0000000..c5d38ff Binary files /dev/null and b/app/src/main/res/drawable/volleyball_profile.png differ diff --git a/app/src/main/res/drawable/weightlifting_profile.png b/app/src/main/res/drawable/weightlifting_profile.png new file mode 100644 index 0000000..643b22a Binary files /dev/null and b/app/src/main/res/drawable/weightlifting_profile.png differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index baadf24..d111f3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,8 @@ apollo = "4.1.1" material3 = "1.4.0-alpha11" runtime = "1.9.4" ui = "1.9.4" -foundation = "1.9.4" +foundationVersion = "1.10.3" +material3Version = "1.4.0" [libraries] @@ -34,7 +35,8 @@ androidx-activity = { group = "androidx.activity", name = "activity", version.re androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } -androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } [plugins]