From 054f0016d40bbc9234a147f3ab2ddd6d48ab2f54 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Fri, 19 Sep 2025 15:49:43 -0400 Subject: [PATCH 1/9] git issue fix --- app/build.gradle.kts | 1 + .../com/cornellappdev/score/screen/GameDetailsScreen.kt | 7 ++++++- .../cornellappdev/score/screen/GameScoreSummaryScreen.kt | 3 ++- gradle/libs.versions.toml | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fca0fe5..d6c534f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.8.2") implementation(libs.material3) implementation("com.google.dagger:hilt-android:2.51.1") + implementation(libs.androidx.material3) kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("com.google.accompanist:accompanist-pager:0.24.0-alpha") 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 d2f01bf..4062de6 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt @@ -3,6 +3,7 @@ package com.cornellappdev.score.screen import ScoringSummary import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -106,6 +107,7 @@ fun GameDetailsContent( gameCard: DetailsCardData, navigateToGameScoreSummary: (List) -> Unit ) { + val scrollState = rememberScrollState() Column( modifier = Modifier .background(White) @@ -132,7 +134,10 @@ fun GameDetailsContent( ) Text( text = gameCard.title, - style = heading1.copy(color = GrayPrimary) + style = heading1.copy(color = GrayPrimary), + maxLines = 1, + modifier = Modifier + .horizontalScroll(scrollState) ) Spacer(modifier = Modifier.height(13.5.dp)) diff --git a/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt index 0c94504..a9514f9 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -43,7 +44,7 @@ fun GameScoreSummaryScreenDetail(scoreEvents: List, onBackArrow: () ) { items(scoreEvents.size) { event -> ScoreEventItemDetailed(event = scoreEvents[event]) - Divider(color = Color.LightGray, thickness = 0.5.dp) + HorizontalDivider(thickness = 0.5.dp, color = Color.LightGray) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0fa6263..b53ee5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ media3CommonKtx = "1.5.1" # Using alpha version due to bug with pull to refresh in the latest stabel version # See https://stackoverflow.com/a/79126321 material3 = "1.4.0-alpha11" +material3Version = "1.3.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -31,6 +32,7 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime" } androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From d1e732c9c340a6e9f509f452bf7e032222fbece1 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Tue, 30 Sep 2025 14:04:49 -0400 Subject: [PATCH 2/9] advanced fitler first iteration --- app/build.gradle.kts | 4 +- .../score/components/ButtonPrimary.kt | 11 +- .../score/components/ExpandableSection.kt | 66 ++++++++++++ .../cornellappdev/score/screen/HomeScreen.kt | 102 +++++++++++++++--- app/src/main/res/drawable/advanced_filter.xml | 13 +++ app/src/main/res/drawable/ic_round_minus.xml | 9 ++ app/src/main/res/drawable/ic_round_plus.xml | 9 ++ build.gradle.kts | 3 +- 8 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt create mode 100644 app/src/main/res/drawable/advanced_filter.xml create mode 100644 app/src/main/res/drawable/ic_round_minus.xml create mode 100644 app/src/main/res/drawable/ic_round_plus.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6c534f..182d1ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("com.google.dagger.hilt.android") id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" // this version matches your Kotlin version id("org.jetbrains.kotlin.plugin.serialization") - + id("com.google.gms.google-services") } @@ -94,6 +94,8 @@ dependencies { 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") } apollo { diff --git a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt index 6c9b046..0e6e074 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt @@ -18,9 +18,16 @@ import com.cornellappdev.score.theme.Style.bodyMedium import com.cornellappdev.score.theme.White @Composable -fun ButtonPrimary(text: String, icon: Painter?, onClick: () -> Unit = {}) { - Button(onClick = onClick, +fun ButtonPrimary( + text: String, + icon: Painter?, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Button( + onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = CrimsonPrimary), + modifier = modifier, contentPadding = PaddingValues(12.dp) ) { if (icon != null) { diff --git a/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt new file mode 100644 index 0000000..7c1c671 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt @@ -0,0 +1,66 @@ +package com.cornellappdev.score.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +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.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R + +@Composable +fun ExpandableSection(title: String, options: List) { + var expanded by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf("Under $20") } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, fontSize = 18.sp) + Icon( + painter = painterResource( + id = if (expanded) R.drawable.ic_round_minus else R.drawable.ic_round_plus + ), + contentDescription = if (expanded) "Collapse" else "Expand" + ) + } + + if (expanded) { + Column { + options.forEach { option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { selectedOption = option } + .padding(vertical = 4.dp) + ) { + RadioButton( + selected = (selectedOption == option), + onClick = { selectedOption = option } + ) + Text(option) + } + } + } + } + } +} \ No newline at end of file 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 9e42a06..9894e2f 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -2,10 +2,12 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -15,15 +17,23 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +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.graphics.Color 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 androidx.hilt.navigation.compose.hiltViewModel +import com.cornellappdev.score.components.ButtonPrimary import com.cornellappdev.score.components.EmptyStateBox import com.cornellappdev.score.components.ErrorState import com.cornellappdev.score.components.GameCard @@ -45,13 +55,15 @@ import com.cornellappdev.score.util.sportSelectionList import com.cornellappdev.score.viewmodel.HomeUiState import com.cornellappdev.score.viewmodel.HomeViewModel +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel(), navigateToGameDetails: (String) -> Unit = {} ) { val uiState = homeViewModel.collectUiStateValue() - + var showBottomSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), modifier = Modifier @@ -72,10 +84,56 @@ fun HomeScreen( onGenderSelected = { homeViewModel.onGenderSelected(it) }, onSportSelected = { homeViewModel.onSportSelected(it) }, navigateToGameDetails = navigateToGameDetails, - onRefresh = { homeViewModel.onRefresh() } + onRefresh = { homeViewModel.onRefresh() }, + onAdvFilterClick = { showBottomSheet = true } ) } } + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showBottomSheet = false }, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 32.dp, bottom = 12.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + ExpandableSection( + title = "Price", + options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") + ) + ExpandableSection( + title = "Location", + options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") + ) + ExpandableSection( + title = "Date of Game", + options = listOf("Today", "Within 7 Days", "Within a Month", "Over a Month") + ) + ButtonPrimary( + text = "Apply", + icon = null, + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO: Apply filter logic via ViewModel + showBottomSheet = false + } + ) + Text( + "Reset", + fontSize = 14.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clickable { + // TODO: Reset filter logic + showBottomSheet = false + } + ) + } + } + } } } @@ -86,10 +144,17 @@ private fun HomeContent( onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, onRefresh: () -> Unit, - navigateToGameDetails: (String) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {}, + onAdvFilterClick: () -> Unit ) { ScorePullToRefreshBox(isRefreshing = uiState.loadedState == ApiResponse.Loading, onRefresh) { - HomeLazyColumn(uiState, onGenderSelected, onSportSelected, navigateToGameDetails) + HomeLazyColumn( + uiState, + onGenderSelected, + onSportSelected, + navigateToGameDetails, + onAdvFilterClick + ) } } @@ -99,7 +164,8 @@ private fun HomeLazyColumn( uiState: HomeUiState, onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, - navigateToGameDetails: (String) -> Unit + navigateToGameDetails: (String) -> Unit, + onAdvFilterClick: () -> Unit ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { if (uiState.filteredGames.isNotEmpty()) { @@ -129,12 +195,22 @@ private fun HomeLazyColumn( .padding(horizontal = 24.dp) ) { Spacer(Modifier.height(24.dp)) - Text( - text = "Game Schedule", - style = title, - modifier = Modifier - .fillMaxWidth() - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Game Schedule", + style = title, + ) + ButtonPrimary( + "", + painterResource(id = com.cornellappdev.score.R.drawable.advanced_filter) + ) { + onAdvFilterClick() + } + } Spacer(modifier = Modifier.height(8.dp)) SportSelectorHeader( sports = uiState.selectionList, @@ -199,6 +275,7 @@ private fun HomeScreenPreview() = ScorePreview { onGenderSelected = {}, onSportSelected = {}, onRefresh = {}, + onAdvFilterClick = {} ) } } @@ -215,7 +292,8 @@ private fun HomeScreenEmptyStatePreview() = ScorePreview { ), onGenderSelected = {}, onSportSelected = {}, - onRefresh = {} + onRefresh = {}, + onAdvFilterClick = {} ) } diff --git a/app/src/main/res/drawable/advanced_filter.xml b/app/src/main/res/drawable/advanced_filter.xml new file mode 100644 index 0000000..62010d4 --- /dev/null +++ b/app/src/main/res/drawable/advanced_filter.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_minus.xml b/app/src/main/res/drawable/ic_round_minus.xml new file mode 100644 index 0000000..cbca170 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_plus.xml b/app/src/main/res/drawable/ic_round_plus.xml new file mode 100644 index 0000000..eb042ed --- /dev/null +++ b/app/src/main/res/drawable/ic_round_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/build.gradle.kts b/build.gradle.kts index 61d5852..648673c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.androidApplication) apply false - alias(libs.plugins.jetbrainsKotlinAndroid)version "1.9.10" apply false + alias(libs.plugins.jetbrainsKotlinAndroid) version "1.9.10" apply false id("com.google.dagger.hilt.android") version "2.51.1" apply false kotlin("jvm") version "2.0.20" kotlin("plugin.serialization") version "2.0.20" + id("com.google.gms.google-services") version "4.4.3" apply false } From bf86fc9294b65dbdd9fddb6f083ef46ee3162a1f Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 1 Oct 2025 17:20:05 -0400 Subject: [PATCH 3/9] center icon --- .../java/com/cornellappdev/score/components/ButtonPrimary.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt index 0e6e074..4aa4108 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt @@ -39,7 +39,9 @@ fun ButtonPrimary( .height(24.dp), colorFilter = ColorFilter.tint(White) ) - Spacer(modifier = Modifier.width(8.dp)) + if (text.isNotEmpty()) { + Spacer(modifier = Modifier.width(8.dp)) + } } Text(text = text, style = bodyMedium.copy(color = White)) } From 2f2017ddc0727f58b8633856530750895bde480b Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 1 Oct 2025 17:49:14 -0400 Subject: [PATCH 4/9] google-services.json --- app/src/google-services.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/src/google-services.json diff --git a/app/src/google-services.json b/app/src/google-services.json new file mode 100644 index 0000000..6d64378 --- /dev/null +++ b/app/src/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1041954613554", + "project_id": "score-2bbd4", + "storage_bucket": "score-2bbd4.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1041954613554:android:afdaffd5c956622603cf2b", + "android_client_info": { + "package_name": "com.cornellappdev.score" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCjJpkMnpwSVu270_k6_3UmaHXb_NiCc0I" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file From 3cc61b7b8a20b794ee114a683a736bea5ffc001b Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Thu, 2 Oct 2025 11:56:18 -0400 Subject: [PATCH 5/9] fix scrollable --- .../cornellappdev/score/screen/HomeScreen.kt | 84 ++++++++++++------- 1 file changed, 53 insertions(+), 31 deletions(-) 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 9894e2f..459abb3 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 @@ -36,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.components.ButtonPrimary import com.cornellappdev.score.components.EmptyStateBox import com.cornellappdev.score.components.ErrorState +import com.cornellappdev.score.components.ExpandableSection import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.LoadingScreen @@ -94,49 +96,69 @@ fun HomeScreen( onDismissRequest = { showBottomSheet = false }, sheetState = sheetState ) { - Column( + LazyColumn( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 32.dp, bottom = 12.dp), + .padding(horizontal = 16.dp), + contentPadding = PaddingValues( + top = 32.dp, + bottom = 24.dp + ), verticalArrangement = Arrangement.spacedBy(20.dp) ) { - ExpandableSection( - title = "Price", - options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") - ) - ExpandableSection( - title = "Location", - options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") - ) - ExpandableSection( - title = "Date of Game", - options = listOf("Today", "Within 7 Days", "Within a Month", "Over a Month") - ) - ButtonPrimary( - text = "Apply", - icon = null, - modifier = Modifier.fillMaxWidth(), - onClick = { - // TODO: Apply filter logic via ViewModel - showBottomSheet = false - } - ) - Text( - "Reset", - fontSize = 14.sp, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .clickable { - // TODO: Reset filter logic + item { + ExpandableSection( + title = "Price", + options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") + ) + } + item { + ExpandableSection( + title = "Location", + options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") + ) + } + item { + ExpandableSection( + title = "Date of Game", + options = listOf( + "Today", + "Within 7 Days", + "Within a Month", + "Over a Month" + ) + ) + } + item { + ButtonPrimary( + text = "Apply", + icon = null, + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO: Apply filter logic via ViewModel showBottomSheet = false } - ) + ) + } + item { + Text( + "Reset", fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .clickable { + // TODO: Reset filter logic + showBottomSheet = false + }, + textAlign = TextAlign.Center + ) + } } } } } } + @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable private fun HomeContent( From 6fef02b5bfb033702e53c58815ff9a718929190a Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Mon, 27 Oct 2025 10:51:08 -0400 Subject: [PATCH 6/9] pagination --- app/build.gradle.kts | 5 +- app/src/main/graphql/FragmentedGame.graphql | 33 ++++++ app/src/main/graphql/schema.graphqls | 55 ++++++++- .../score/model/ScoreRepository.kt | 106 ++++++++++++++++-- 4 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 app/src/main/graphql/FragmentedGame.graphql diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 182d1ca..5b068a7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,7 @@ 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 @@ -88,9 +88,8 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling") androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - implementation(libs.apollo.runtime) 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) diff --git a/app/src/main/graphql/FragmentedGame.graphql b/app/src/main/graphql/FragmentedGame.graphql new file mode 100644 index 0000000..4f5d398 --- /dev/null +++ b/app/src/main/graphql/FragmentedGame.graphql @@ -0,0 +1,33 @@ +query PagedGames($limit: Int!, $offset: Int!) { + games(limit: $limit, offset: $offset) { + id + city + date + gender + location + opponentId + result + sport + state + time + scoreBreakdown + utcDate + team { + id + color + image + name + } + boxScore { + team + period + time + description + scorer + assist + scoreBy + corScore + oppScore + } + } +} diff --git a/app/src/main/graphql/schema.graphqls b/app/src/main/graphql/schema.graphqls index 0265edb..e1ebfad 100644 --- a/app/src/main/graphql/schema.graphqls +++ b/app/src/main/graphql/schema.graphqls @@ -1,13 +1,15 @@ type Query { + articles(sportsType: String): [ArticleType] + youtubeVideos: [YoutubeVideoType] youtubeVideo(id: String!): YoutubeVideoType - games: [GameType] + games("Number of games to return" limit: Int = 100, "Number of games to skip" offset: Int = 0): [GameType] game(id: String!): GameType - gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType + gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!, ticketLink: String): GameType gamesBySport(sport: String!): [GameType] @@ -22,6 +24,30 @@ type Query { teamByName(name: String!): TeamType } +""" +A GraphQL type representing a news article. + +Attributes: + - title: The title of the article + - image: The filename of the article's main image + - sports_type: The specific sport category + - published_at: The publication date + - url: The URL to the full article +""" +type ArticleType { + id: String + + title: String! + + image: String + + sportsType: String! + + publishedAt: String! + + url: String! +} + """ A GraphQL type representing a YouTube video. @@ -42,6 +68,8 @@ type YoutubeVideoType { thumbnail: String! + b64Thumbnail: String! + url: String! publishedAt: String! @@ -63,6 +91,7 @@ Attributes: - `time`: The time of the game. (optional) - `box_score`: The box score of the game. - `score_breakdown`: The score breakdown of the game. + - `ticket_link`: The ticket link of the game. (optional) """ type GameType { id: String @@ -90,6 +119,10 @@ type GameType { scoreBreakdown: [[String]] team: TeamType + + utcDate: String + + ticketLink: String } """ @@ -133,6 +166,7 @@ Attributes: - `id`: The ID of the team (optional). - `color`: The color of the team. - `image`: The image of the team (optional). + - `b64_image`: The base64 encoded image of the team (optional). - `name`: The name of the team. """ type TeamType { @@ -142,6 +176,8 @@ type TeamType { image: String + b64Image: String + name: String! } @@ -149,17 +185,22 @@ type Mutation { """ Creates a new game. """ - createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!): CreateGame + createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, ticketLink: String, time: String!, utcDate: String): CreateGame """ Creates a new team. """ - createTeam(color: String!, image: String, name: String!): CreateTeam + createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam """ Creates a new youtube video. """ - createYoutubeVideo(description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + + """ + Creates a new article. + """ + createArticle(image: String, publishedAt: String!, slug: String!, sportsType: String!, title: String!, url: String!): CreateArticle } type CreateGame { @@ -174,6 +215,10 @@ type CreateYoutubeVideo { youtubeVideo: YoutubeVideoType } +type CreateArticle { + article: ArticleType +} + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation and subscription operations. """ 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 1310cd4..5d283ef 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -2,10 +2,12 @@ package com.cornellappdev.score.model import android.util.Log import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.exception.ApolloException import com.cornellappdev.score.util.isValidSport import com.cornellappdev.score.util.parseColor import com.example.score.GameByIdQuery import com.example.score.GamesQuery +import com.example.score.PagedGamesQuery import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -36,11 +38,17 @@ class ScoreRepository @Inject constructor( MutableStateFlow>(ApiResponse.Loading) val currentGamesFlow = _currentGameFlow.asStateFlow() + companion object { + private const val PAGE_LIMIT = 100 + private const val MAX_RETRIES = 3 + private const val PAGE_TIMEOUT_MILLIS = 3000L + } + /** * Asynchronously fetches the list of games from the API. Once finished, will send down * `upcomingGamesFlow` to be observed. */ - fun fetchGames() = appScope.launch { + fun fetchGamesPrev() = appScope.launch { _upcomingGamesFlow.value = ApiResponse.Loading try { val result = @@ -92,6 +100,79 @@ class ScoreRepository @Inject constructor( } } + fun fetchGames() = appScope.launch { + _upcomingGamesFlow.value = ApiResponse.Loading + val allGames = mutableListOf() + var offset = 0 + var retries = 0 + + try { + while (true) { + val pageResult: List? = try { + withTimeout(PAGE_TIMEOUT_MILLIS) { + apolloClient.query(PagedGamesQuery(limit = PAGE_LIMIT, offset = offset)) + .execute() + .data + ?.games + } + } catch (e: Exception) { + null + } + + if (pageResult == null) { + if (retries < MAX_RETRIES) { + retries++ + continue + } else { + break + } + } + + if (pageResult.isEmpty()) { + break + } + + retries = 0 + + val pageGames: List = pageResult + .filterNotNull() + .filter { gql -> isValidSport(gql.sport ?: "") } + .mapNotNull { gql -> + val scores = gql.result?.split(",")?.getOrNull(1)?.split("-") + val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() + val otherScore = scores?.getOrNull(1)?.toNumberOrNull() + gql.team?.image?.let { imageUrl -> + Game( + id = gql.id ?: "", + teamLogo = imageUrl, + teamName = gql.team.name, + teamColor = parseColor(gql.team.color).copy(alpha = 0.4f * 255), + gender = if (gql.gender == "Mens") "Men's" else "Women's", + sport = gql.sport, + date = gql.date, + city = gql.city, + cornellScore = cornellScore, + otherScore = otherScore + ) + } + } + + allGames.addAll(pageGames) + + if (pageResult.size < PAGE_LIMIT) break + offset += PAGE_LIMIT + } + + _upcomingGamesFlow.value = + if (allGames.isNotEmpty()) ApiResponse.Success(allGames) + else ApiResponse.Error + + } catch (e: Exception) { + Log.e("ScoreRepository", "Error fetching upcoming games", e) + _upcomingGamesFlow.value = ApiResponse.Error + } + } + /** * Asynchronously fetches game details for a particular game. Once finished, will update * `currentGamesFlow` to be observed. @@ -99,17 +180,26 @@ class ScoreRepository @Inject constructor( fun getGameById(id: String) = appScope.launch { _currentGameFlow.value = ApiResponse.Loading try { - val result = + val response = withTimeout(TIMEOUT_TIME_MILLIS) { - apolloClient.query(GameByIdQuery(id)).execute().toResult() + apolloClient.query(GameByIdQuery(id)).execute() } + if (response.hasErrors()) { + Log.e("ScoreRepository", "Error fetching game with id: $id: ${response.errors}") + _currentGameFlow.value = ApiResponse.Error + return@launch + } - result.getOrNull()?.game?.let { + response.data?.game?.let { _currentGameFlow.value = ApiResponse.Success(it.toGameDetails()) } ?: _currentGameFlow.update { ApiResponse.Error } + + } catch (e: ApolloException) { + Log.e("ScoreRepository", "Error fetching game with id: $id: ", e) + _currentGameFlow.value = ApiResponse.Error } catch (e: Exception) { - Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e) + Log.e("ScoreRepository", "A timeout or other error occurred for game id: $id", e) _currentGameFlow.value = ApiResponse.Error } } @@ -117,8 +207,6 @@ class ScoreRepository @Inject constructor( } fun String.toNumberOrNull(): Number? { - return when { - this.contains(".") -> this.toFloatOrNull() // Try converting to Float if there's a decimal - else -> this.toIntOrNull() // Otherwise, try converting to Int - } + return this.trim().toFloatOrNull() ?: this.trim().toIntOrNull() } + From 555d04dcca35e722ddbb9bb730cbf66e44141aec Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Sun, 9 Nov 2025 03:12:42 -0500 Subject: [PATCH 7/9] test --- .github/workflows/android-lint.yml | 7 +- app/build.gradle.kts | 1 + app/src/google-services.json | 29 ------- .../score/components/CustomRadioButton.kt | 29 +++++++ .../score/components/ExpandableSection.kt | 80 ++++++++++++++----- .../score/components/IconButton.kt | 48 +++++++++++ .../score/model/ScoreRepository.kt | 9 +-- .../cornellappdev/score/screen/HomeScreen.kt | 50 +++++++----- .../main/res/drawable/ic_radio_selected.xml | 16 ++++ .../main/res/drawable/ic_radio_unselected.xml | 11 +++ gradle/libs.versions.toml | 2 + 11 files changed, 205 insertions(+), 77 deletions(-) delete mode 100644 app/src/google-services.json create mode 100644 app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt create mode 100644 app/src/main/java/com/cornellappdev/score/components/IconButton.kt create mode 100644 app/src/main/res/drawable/ic_radio_selected.xml create mode 100644 app/src/main/res/drawable/ic_radio_unselected.xml diff --git a/.github/workflows/android-lint.yml b/.github/workflows/android-lint.yml index 968fbe3..9da11f7 100644 --- a/.github/workflows/android-lint.yml +++ b/.github/workflows/android-lint.yml @@ -15,6 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 + - name: set up JDK 17 uses: actions/setup-java@v3 with: @@ -24,4 +25,8 @@ jobs: - name: Run Lint run: ./gradlew lint - continue-on-error: false \ No newline at end of file + continue-on-error: false + + - name: Create google-services.json + run: | + echo "${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}" | base64 --decode > app/google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5b068a7..6780f91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { implementation(libs.material3) implementation("com.google.dagger:hilt-android:2.51.1") implementation(libs.androidx.material3) + implementation(libs.androidx.foundation) kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("com.google.accompanist:accompanist-pager:0.24.0-alpha") diff --git a/app/src/google-services.json b/app/src/google-services.json deleted file mode 100644 index 6d64378..0000000 --- a/app/src/google-services.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "project_info": { - "project_number": "1041954613554", - "project_id": "score-2bbd4", - "storage_bucket": "score-2bbd4.firebasestorage.app" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:1041954613554:android:afdaffd5c956622603cf2b", - "android_client_info": { - "package_name": "com.cornellappdev.score" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyCjJpkMnpwSVu270_k6_3UmaHXb_NiCc0I" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt b/app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt new file mode 100644 index 0000000..5c44f82 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt @@ -0,0 +1,29 @@ +package com.cornellappdev.score.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R + +@Composable +fun CustomRadioButton( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Icon( + painter = painterResource( + id = if (selected) R.drawable.ic_radio_selected else R.drawable.ic_radio_unselected + ), + contentDescription = if (selected) "Selected" else "Unselected", + modifier = modifier + .size(20.dp) + .padding(end = 8.dp) + .clickable(onClick = onClick) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt index 7c1c671..4fcf478 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt @@ -1,5 +1,7 @@ package com.cornellappdev.score.components +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -7,7 +9,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon -import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -18,13 +19,43 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.cornellappdev.score.R +import com.cornellappdev.score.theme.Style.bodyNormal +import com.cornellappdev.score.theme.Style.heading2 + +interface DisplayableFilter { + val displayName: String +} + +enum class PriceFilter(override val displayName: String) : DisplayableFilter { + UNTICKETED("Unticketed"), + UNDER_20("Under $20"), + UNDER_50("Under $50"), + OVER_50("Over $50") +} + +enum class LocationFilter(override val displayName: String) : DisplayableFilter { + ON_CAMPUS("On Campus"), + ONE_TO_TWO_HOURS("1-2 Hours"), + TWO_TO_FOUR_HOURS("2-4 Hours"), + OVER_FOUR_HOURS("Over 4 Hours") +} + +enum class DateFilter(override val displayName: String) : DisplayableFilter { + TODAY("Today"), + WITHIN_7_DAYS("Within 7 Days"), + WITHIN_A_MONTH("Within a Month"), + OVER_A_MONTH("Over a Month") +} @Composable -fun ExpandableSection(title: String, options: List) { +fun ExpandableSection( + title: String, + options: List, + selectedOption: T?, + onOptionSelected: (T?) -> Unit +) { var expanded by remember { mutableStateOf(false) } - var selectedOption by remember { mutableStateOf("Under $20") } Column { Row( @@ -34,7 +65,7 @@ fun ExpandableSection(title: String, options: List) { horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(title, fontSize = 18.sp) + Text(title, style = heading2) Icon( painter = painterResource( id = if (expanded) R.drawable.ic_round_minus else R.drawable.ic_round_plus @@ -43,24 +74,31 @@ fun ExpandableSection(title: String, options: List) { ) } - if (expanded) { - Column { - options.forEach { option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { selectedOption = option } - .padding(vertical = 4.dp) - ) { - RadioButton( - selected = (selectedOption == option), - onClick = { selectedOption = option } - ) - Text(option) + // Options + Column( + modifier = Modifier + .fillMaxWidth() + .animateContentSize() + ) { + AnimatedVisibility(visible = expanded) { + Column { + options.forEach { option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { onOptionSelected(option) } + .padding(start = 16.dp, top = 4.dp, bottom = 4.dp) + ) { + CustomRadioButton( + selected = (selectedOption == option), + onClick = { onOptionSelected(option) } + ) + Text(option.displayName, style = bodyNormal) + } } } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/score/components/IconButton.kt b/app/src/main/java/com/cornellappdev/score/components/IconButton.kt new file mode 100644 index 0000000..b156faf --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/IconButton.kt @@ -0,0 +1,48 @@ +package com.cornellappdev.score.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +@Composable +fun IconButton( + icon: Painter, + contentDescription: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, + tint: Color = Color.Unspecified, + borderColor: Color = Color(0xFFD6D6D6) +) { + Surface( + modifier = modifier + .clickable(onClick = onClick), + shape = RoundedCornerShape(100.dp), + border = BorderStroke(1.dp, borderColor), + color = Color.Transparent + ) { + Box( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 4.dp) + .size(height = 32.dp, width = 32.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painter = icon, + contentDescription = contentDescription, + modifier = Modifier.size(24.dp), + tint = tint + ) + } + } +} 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 5d283ef..aa73478 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -18,6 +18,9 @@ import javax.inject.Inject import javax.inject.Singleton private const val TIMEOUT_TIME_MILLIS = 5000L +private const val PAGE_LIMIT = 100 +private const val MAX_RETRIES = 3 +private const val PAGE_TIMEOUT_MILLIS = 3000L /** * This is a singleton responsible for fetching and caching all data for Score. @@ -38,12 +41,6 @@ class ScoreRepository @Inject constructor( MutableStateFlow>(ApiResponse.Loading) val currentGamesFlow = _currentGameFlow.asStateFlow() - companion object { - private const val PAGE_LIMIT = 100 - private const val MAX_RETRIES = 3 - private const val PAGE_TIMEOUT_MILLIS = 3000L - } - /** * Asynchronously fetches the list of games from the API. Once finished, will send down * `upcomingGamesFlow` to be observed. 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 459abb3..e516631 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -34,13 +34,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.cornellappdev.score.R import com.cornellappdev.score.components.ButtonPrimary +import com.cornellappdev.score.components.DateFilter import com.cornellappdev.score.components.EmptyStateBox import com.cornellappdev.score.components.ErrorState import com.cornellappdev.score.components.ExpandableSection import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.GamesCarousel +import com.cornellappdev.score.components.IconButton import com.cornellappdev.score.components.LoadingScreen +import com.cornellappdev.score.components.LocationFilter +import com.cornellappdev.score.components.PriceFilter import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.components.ScorePullToRefreshBox import com.cornellappdev.score.components.SportSelectorHeader @@ -87,11 +92,14 @@ fun HomeScreen( onSportSelected = { homeViewModel.onSportSelected(it) }, navigateToGameDetails = navigateToGameDetails, onRefresh = { homeViewModel.onRefresh() }, - onAdvFilterClick = { showBottomSheet = true } + onAdvancedFilterClick = { showBottomSheet = true } ) } } if (showBottomSheet) { + var selectedPrice by remember { mutableStateOf(null) } + var selectedLocation by remember { mutableStateOf(null) } + var selectedDate by remember { mutableStateOf(null) } ModalBottomSheet( onDismissRequest = { showBottomSheet = false }, sheetState = sheetState @@ -109,24 +117,27 @@ fun HomeScreen( item { ExpandableSection( title = "Price", - options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") + options = PriceFilter.entries, + selectedOption = selectedPrice, + onOptionSelected = { selectedPrice = it } ) } + item { ExpandableSection( title = "Location", - options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") + options = LocationFilter.entries, + selectedOption = selectedLocation, + onOptionSelected = { selectedLocation = it } ) } + item { ExpandableSection( title = "Date of Game", - options = listOf( - "Today", - "Within 7 Days", - "Within a Month", - "Over a Month" - ) + options = DateFilter.entries, + selectedOption = selectedDate, + onOptionSelected = { selectedDate = it } ) } item { @@ -167,7 +178,7 @@ private fun HomeContent( onSportSelected: (SportSelection) -> Unit, onRefresh: () -> Unit, navigateToGameDetails: (String) -> Unit = {}, - onAdvFilterClick: () -> Unit + onAdvancedFilterClick: () -> Unit ) { ScorePullToRefreshBox(isRefreshing = uiState.loadedState == ApiResponse.Loading, onRefresh) { HomeLazyColumn( @@ -175,7 +186,7 @@ private fun HomeContent( onGenderSelected, onSportSelected, navigateToGameDetails, - onAdvFilterClick + onAdvancedFilterClick ) } } @@ -187,7 +198,7 @@ private fun HomeLazyColumn( onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, navigateToGameDetails: (String) -> Unit, - onAdvFilterClick: () -> Unit + onAdvancedFilterClick: () -> Unit ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { if (uiState.filteredGames.isNotEmpty()) { @@ -226,12 +237,11 @@ private fun HomeLazyColumn( text = "Game Schedule", style = title, ) - ButtonPrimary( - "", - painterResource(id = com.cornellappdev.score.R.drawable.advanced_filter) - ) { - onAdvFilterClick() - } + IconButton( + icon = painterResource(id = R.drawable.advanced_filter), + contentDescription = "Advanced filter", + onClick = onAdvancedFilterClick + ) } Spacer(modifier = Modifier.height(8.dp)) SportSelectorHeader( @@ -297,7 +307,7 @@ private fun HomeScreenPreview() = ScorePreview { onGenderSelected = {}, onSportSelected = {}, onRefresh = {}, - onAdvFilterClick = {} + onAdvancedFilterClick = {} ) } } @@ -315,7 +325,7 @@ private fun HomeScreenEmptyStatePreview() = ScorePreview { onGenderSelected = {}, onSportSelected = {}, onRefresh = {}, - onAdvFilterClick = {} + onAdvancedFilterClick = {} ) } diff --git a/app/src/main/res/drawable/ic_radio_selected.xml b/app/src/main/res/drawable/ic_radio_selected.xml new file mode 100644 index 0000000..14a7116 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_selected.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_radio_unselected.xml b/app/src/main/res/drawable/ic_radio_unselected.xml new file mode 100644 index 0000000..291ddad --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_unselected.xml @@ -0,0 +1,11 @@ + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b53ee5a..17726ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ media3CommonKtx = "1.5.1" # See https://stackoverflow.com/a/79126321 material3 = "1.4.0-alpha11" material3Version = "1.3.2" +foundation = "1.9.4" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -33,6 +34,7 @@ androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime" } androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } +androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From 6f623d9b59141f1c14f125899debc2ec37813922 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Sun, 9 Nov 2025 03:24:25 -0500 Subject: [PATCH 8/9] second test --- .github/workflows/android-lint.yml | 7 +++--- app/.gitignore | 3 ++- .../score/model/ScoreRepository.kt | 24 +++++++------------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.github/workflows/android-lint.yml b/.github/workflows/android-lint.yml index 9da11f7..e329021 100644 --- a/.github/workflows/android-lint.yml +++ b/.github/workflows/android-lint.yml @@ -22,11 +22,12 @@ jobs: java-version: '17' distribution: 'temurin' cache: gradle + + - name: Create google-services.json + run: | + echo "${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}" | base64 --decode > app/google-services.json - name: Run Lint run: ./gradlew lint continue-on-error: false - - name: Create google-services.json - run: | - echo "${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}" | base64 --decode > app/google-services.json diff --git a/app/.gitignore b/app/.gitignore index 42afabf..7ecd011 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ -/build \ No newline at end of file +/build +app/google-services.json 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 aa73478..52e08e3 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -2,7 +2,6 @@ package com.cornellappdev.score.model import android.util.Log import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.exception.ApolloException import com.cornellappdev.score.util.isValidSport import com.cornellappdev.score.util.parseColor import com.example.score.GameByIdQuery @@ -177,33 +176,26 @@ class ScoreRepository @Inject constructor( fun getGameById(id: String) = appScope.launch { _currentGameFlow.value = ApiResponse.Loading try { - val response = + val result = withTimeout(TIMEOUT_TIME_MILLIS) { - apolloClient.query(GameByIdQuery(id)).execute() + apolloClient.query(GameByIdQuery(id)).execute().toResult() } - if (response.hasErrors()) { - Log.e("ScoreRepository", "Error fetching game with id: $id: ${response.errors}") - _currentGameFlow.value = ApiResponse.Error - return@launch - } - response.data?.game?.let { + result.getOrNull()?.game?.let { _currentGameFlow.value = ApiResponse.Success(it.toGameDetails()) } ?: _currentGameFlow.update { ApiResponse.Error } - - } catch (e: ApolloException) { - Log.e("ScoreRepository", "Error fetching game with id: $id: ", e) - _currentGameFlow.value = ApiResponse.Error } catch (e: Exception) { - Log.e("ScoreRepository", "A timeout or other error occurred for game id: $id", e) + Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e) _currentGameFlow.value = ApiResponse.Error } } - } fun String.toNumberOrNull(): Number? { - return this.trim().toFloatOrNull() ?: this.trim().toIntOrNull() + return when { + this.contains(".") -> this.toFloatOrNull() // Try converting to Float if there's a decimal + else -> this.toIntOrNull() // Otherwise, try converting to Int + } } From acf1fe2be7a737c2c176574822ceb37817d87da3 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 19 Nov 2025 17:09:53 -0500 Subject: [PATCH 9/9] pr comments --- app/build.gradle.kts | 5 +- .../score/components/CustomRadioButton.kt | 29 ------ .../score/components/ExpandableSection.kt | 27 +++++- .../score/components/IconButton.kt | 23 ++++- .../score/components/ScoreRadioButton.kt | 44 +++++++++ .../score/model/ScoreRepository.kt | 33 +++---- .../cornellappdev/score/screen/HomeScreen.kt | 94 +++++++++---------- gradle/libs.versions.toml | 4 +- 8 files changed, 149 insertions(+), 110 deletions(-) delete mode 100644 app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt create mode 100644 app/src/main/java/com/cornellappdev/score/components/ScoreRadioButton.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6780f91..6ecc345 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) version "1.9.10" - id("com.apollographql.apollo") version "4.0.0" + alias(libs.plugins.apollo) 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 @@ -73,6 +73,7 @@ dependencies { implementation("com.google.dagger:hilt-android:2.51.1") implementation(libs.androidx.material3) implementation(libs.androidx.foundation) + implementation(libs.androidx.compose.material3.material3) kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("com.google.accompanist:accompanist-pager:0.24.0-alpha") @@ -90,7 +91,7 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation("com.apollographql.apollo:apollo-runtime:4.0.0") + implementation(libs.apollo.runtime) 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) diff --git a/app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt b/app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt deleted file mode 100644 index 5c44f82..0000000 --- a/app/src/main/java/com/cornellappdev/score/components/CustomRadioButton.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.cornellappdev.score.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.cornellappdev.score.R - -@Composable -fun CustomRadioButton( - selected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Icon( - painter = painterResource( - id = if (selected) R.drawable.ic_radio_selected else R.drawable.ic_radio_unselected - ), - contentDescription = if (selected) "Selected" else "Unselected", - modifier = modifier - .size(20.dp) - .padding(end = 8.dp) - .clickable(onClick = onClick) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt index 4fcf478..f1cf06b 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt @@ -1,6 +1,5 @@ package com.cornellappdev.score.components -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -8,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,6 +18,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.theme.Style.bodyNormal @@ -80,7 +81,7 @@ fun ExpandableSection( .fillMaxWidth() .animateContentSize() ) { - AnimatedVisibility(visible = expanded) { + if (expanded) { Column { options.forEach { option -> Row( @@ -90,9 +91,12 @@ fun ExpandableSection( .clickable { onOptionSelected(option) } .padding(start = 16.dp, top = 4.dp, bottom = 4.dp) ) { - CustomRadioButton( + ScoreRadioButton( selected = (selectedOption == option), - onClick = { onOptionSelected(option) } + onClick = { onOptionSelected(option) }, + modifier = Modifier + .size(20.dp) + .padding(end = 8.dp) ) Text(option.displayName, style = bodyNormal) } @@ -102,3 +106,18 @@ fun ExpandableSection( } } } + +@Preview(showBackground = true) +@Composable +private fun ExpandableSectionPreview() = ScorePreview { + var selected by remember { mutableStateOf(null) } + + Column(modifier = Modifier.padding(16.dp)) { + ExpandableSection( + title = "Price", + options = PriceFilter.entries, + selectedOption = selected, + onOptionSelected = { selected = it } + ) + } +} diff --git a/app/src/main/java/com/cornellappdev/score/components/IconButton.kt b/app/src/main/java/com/cornellappdev/score/components/IconButton.kt index b156faf..603e466 100644 --- a/app/src/main/java/com/cornellappdev/score/components/IconButton.kt +++ b/app/src/main/java/com/cornellappdev/score/components/IconButton.kt @@ -2,7 +2,9 @@ package com.cornellappdev.score.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -13,7 +15,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R @Composable fun IconButton( @@ -25,16 +30,15 @@ fun IconButton( borderColor: Color = Color(0xFFD6D6D6) ) { Surface( - modifier = modifier - .clickable(onClick = onClick), + modifier = modifier.clickable(onClick = onClick), shape = RoundedCornerShape(100.dp), border = BorderStroke(1.dp, borderColor), color = Color.Transparent ) { Box( modifier = Modifier - .padding(horizontal = 12.dp, vertical = 4.dp) - .size(height = 32.dp, width = 32.dp), + .height(32.dp) + .padding(horizontal = 12.dp, vertical = 4.dp), contentAlignment = Alignment.Center ) { Icon( @@ -46,3 +50,14 @@ fun IconButton( } } } + +@Preview(showBackground = true) +@Composable +private fun IconButtonPreview() = ScorePreview { + IconButton( + icon = painterResource(id = R.drawable.advanced_filter), + contentDescription = "Example Icon", + onClick = {} + ) +} + diff --git a/app/src/main/java/com/cornellappdev/score/components/ScoreRadioButton.kt b/app/src/main/java/com/cornellappdev/score/components/ScoreRadioButton.kt new file mode 100644 index 0000000..0d9d760 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/ScoreRadioButton.kt @@ -0,0 +1,44 @@ +package com.cornellappdev.score.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.theme.GrayPrimary + +@Composable +fun ScoreRadioButton( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + RadioButton( + selected = selected, + onClick = onClick, + modifier = modifier, + colors = RadioButtonDefaults.colors( + selectedColor = GrayPrimary, + unselectedColor = GrayPrimary + ) + ) +} + +@Preview +@Composable +private fun ScoreRadioButtonPreview() = ScorePreview { + Column(modifier = Modifier.padding(16.dp)) { + ScoreRadioButton( + selected = false, + onClick = {} + ) + ScoreRadioButton( + selected = true, + onClick = {} + ) + } +} 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 52e08e3..2b84524 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -104,16 +104,13 @@ class ScoreRepository @Inject constructor( try { while (true) { - val pageResult: List? = try { + val pageResult = runCatching { withTimeout(PAGE_TIMEOUT_MILLIS) { - apolloClient.query(PagedGamesQuery(limit = PAGE_LIMIT, offset = offset)) - .execute() - .data - ?.games + apolloClient.query( + PagedGamesQuery(limit = PAGE_LIMIT, offset = offset) + ).execute().data?.games } - } catch (e: Exception) { - null - } + }.getOrNull() if (pageResult == null) { if (retries < MAX_RETRIES) { @@ -133,20 +130,20 @@ class ScoreRepository @Inject constructor( val pageGames: List = pageResult .filterNotNull() .filter { gql -> isValidSport(gql.sport ?: "") } - .mapNotNull { gql -> - val scores = gql.result?.split(",")?.getOrNull(1)?.split("-") + .mapNotNull { graphqlGame -> + val scores = graphqlGame.result?.split(",")?.getOrNull(1)?.split("-") val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() val otherScore = scores?.getOrNull(1)?.toNumberOrNull() - gql.team?.image?.let { imageUrl -> + graphqlGame.team?.image?.let { imageUrl -> Game( - id = gql.id ?: "", + id = graphqlGame.id ?: "", teamLogo = imageUrl, - teamName = gql.team.name, - teamColor = parseColor(gql.team.color).copy(alpha = 0.4f * 255), - gender = if (gql.gender == "Mens") "Men's" else "Women's", - sport = gql.sport, - date = gql.date, - city = gql.city, + teamName = graphqlGame.team.name, + teamColor = parseColor(graphqlGame.team.color).copy(alpha = 0.4f * 255), + gender = if (graphqlGame.gender == "Mens") "Men's" else "Women's", + sport = graphqlGame.sport, + date = graphqlGame.date, + city = graphqlGame.city, cornellScore = cornellScore, otherScore = otherScore ) 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 e516631..317b2ed 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -104,65 +104,55 @@ fun HomeScreen( onDismissRequest = { showBottomSheet = false }, sheetState = sheetState ) { - LazyColumn( + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - contentPadding = PaddingValues( - top = 32.dp, - bottom = 24.dp - ), + .padding(horizontal = 16.dp) + .padding(top = 32.dp, bottom = 24.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { - item { - ExpandableSection( - title = "Price", - options = PriceFilter.entries, - selectedOption = selectedPrice, - onOptionSelected = { selectedPrice = it } - ) - } + ExpandableSection( + title = "Price", + options = PriceFilter.entries, + selectedOption = selectedPrice, + onOptionSelected = { selectedPrice = it } + ) + + ExpandableSection( + title = "Location", + options = LocationFilter.entries, + selectedOption = selectedLocation, + onOptionSelected = { selectedLocation = it } + ) + + ExpandableSection( + title = "Date of Game", + options = DateFilter.entries, + selectedOption = selectedDate, + onOptionSelected = { selectedDate = it } + ) - item { - ExpandableSection( - title = "Location", - options = LocationFilter.entries, - selectedOption = selectedLocation, - onOptionSelected = { selectedLocation = it } - ) - } + ButtonPrimary( + text = "Apply", + icon = null, + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO: Apply filter logic via ViewModel + showBottomSheet = false + } + ) - item { - ExpandableSection( - title = "Date of Game", - options = DateFilter.entries, - selectedOption = selectedDate, - onOptionSelected = { selectedDate = it } - ) - } - item { - ButtonPrimary( - text = "Apply", - icon = null, - modifier = Modifier.fillMaxWidth(), - onClick = { - // TODO: Apply filter logic via ViewModel + Text( + "Reset", + fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .clickable { + // TODO: Reset filter logic showBottomSheet = false - } - ) - } - item { - Text( - "Reset", fontSize = 14.sp, - modifier = Modifier - .fillMaxWidth() - .clickable { - // TODO: Reset filter logic - showBottomSheet = false - }, - textAlign = TextAlign.Center - ) - } + }, + textAlign = TextAlign.Center + ) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17726ae..0edc01b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ media3CommonKtx = "1.5.1" material3 = "1.4.0-alpha11" material3Version = "1.3.2" foundation = "1.9.4" +androidxMaterial3 = "1.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -31,10 +32,11 @@ material3 = { group = "androidx.compose.material3", name = "material3", version. androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } 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" } +apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +androidx-compose-material3-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxMaterial3" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }