diff --git a/app/src/main/java/com/cornellappdev/score/components/ScorePullToRefreshBox.kt b/app/src/main/java/com/cornellappdev/score/components/ScorePullToRefreshBox.kt new file mode 100644 index 0000000..65b020d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/ScorePullToRefreshBox.kt @@ -0,0 +1,33 @@ +package com.cornellappdev.score.components + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.White + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScorePullToRefreshBox( + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing, onRefresh, modifier, + state = state, + indicator = { + Indicator(state, isRefreshing, color = CrimsonPrimary, containerColor = White) + }, + contentAlignment = Alignment.TopCenter + ) { + content() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt b/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt index cca3322..68b339c 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt @@ -1,12 +1,11 @@ import androidx.compose.foundation.Image 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.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -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 @@ -30,12 +29,10 @@ import com.cornellappdev.score.util.scoreEvents1 @Composable fun ScoringSummary(scoreEvents: List, modifier: Modifier = Modifier) { - LazyColumn( - modifier = modifier.fillMaxWidth() - ) { - items(scoreEvents) { event -> - ScoreEventItem(event) - Divider(color = Color.LightGray, thickness = 0.5.dp) + Column(modifier = modifier) { + scoreEvents.take(3).map { + ScoreEventItem(it) + HorizontalDivider(color = Color.LightGray, thickness = 0.5.dp) } } } @@ -48,7 +45,7 @@ fun ScoreEventItem(event: ScoreEvent) { .padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - if (event.team.name == "COR"){ // TODO: Check if its "COR" for all queries. It is for baseball + if (event.team.name == "COR") { // TODO: Check if its "COR" for all queries. It is for baseball Image( painter = painterResource(R.drawable.cornell_logo), contentDescription = event.team.name, @@ -56,8 +53,7 @@ fun ScoreEventItem(event: ScoreEvent) { .size(40.dp) .padding(end = 12.dp) ) - } - else{ + } else { AsyncImage( model = event.team.logo, contentDescription = event.team.name, // Turn this into a if statement if i know the link for cornell logo 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 554185d..adf39c0 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt @@ -3,7 +3,6 @@ package com.cornellappdev.score.screen import ScoringSummary import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,7 +12,8 @@ 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.material3.CircularProgressIndicator +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,8 +30,11 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.R import com.cornellappdev.score.components.BoxScore import com.cornellappdev.score.components.ButtonPrimary +import com.cornellappdev.score.components.ErrorState +import com.cornellappdev.score.components.GameDetailsLoadingScreen import com.cornellappdev.score.components.GameScoreHeader import com.cornellappdev.score.components.NavigationHeader +import com.cornellappdev.score.components.ScorePullToRefreshBox import com.cornellappdev.score.components.TimeUntilStartCard import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.DetailsCardData @@ -59,38 +62,36 @@ fun GameDetailsScreen( onBackArrow: () -> Unit = {} ) { val uiState = gameDetailsViewModel.collectUiStateValue() - Column( - modifier = Modifier - .fillMaxSize() - .background(White) + ScorePullToRefreshBox( + // We have a separate loading state for this screen so we don't want the refresh indicator + // to persist as the screen loads. + false, + gameDetailsViewModel::onRefresh ) { - NavigationHeader( - title = "Game Details", - onBackPressed = onBackArrow - ) - when (val state = uiState.loadedState) { - is ApiResponse.Loading, ApiResponse.Loading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = GrayPrimary) + Column( + modifier = Modifier + .fillMaxSize() + .background(White) + .verticalScroll(rememberScrollState()) + ) { + NavigationHeader( + title = "Game Details", + onBackPressed = onBackArrow + ) + when (val state = uiState.loadedState) { + is ApiResponse.Loading, ApiResponse.Loading -> { + GameDetailsLoadingScreen() } - } - is ApiResponse.Error -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = "Failed to load game.") + is ApiResponse.Error -> { + ErrorState(gameDetailsViewModel::onRefresh, "Failed to load game details") } - } - is ApiResponse.Success -> { - GameDetailsContent( - gameCard = state.data - ) + is ApiResponse.Success -> { + GameDetailsContent( + gameCard = state.data + ) + } } } } 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 f2a1918..6688183 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -3,6 +3,7 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background 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.Spacer @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding 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.Text import androidx.compose.runtime.Composable @@ -27,6 +29,7 @@ import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.LoadingScreen import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.components.ScorePullToRefreshBox import com.cornellappdev.score.components.SportSelectorHeader import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.GenderDivision @@ -67,20 +70,35 @@ fun HomeScreen( uiState = uiState, onGenderSelected = { homeViewModel.onGenderSelected(it) }, onSportSelected = { homeViewModel.onSportSelected(it) }, - navigateToGameDetails = navigateToGameDetails + navigateToGameDetails = navigateToGameDetails, + onRefresh = { homeViewModel.onRefresh() } ) } } } } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable private fun HomeContent( uiState: HomeUiState, onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, + onRefresh: () -> Unit, navigateToGameDetails: (String) -> Unit = {} +) { + ScorePullToRefreshBox(isRefreshing = uiState.loadedState == ApiResponse.Loading, onRefresh) { + HomeLazyColumn(uiState, onGenderSelected, onSportSelected, navigateToGameDetails) + } +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun HomeLazyColumn( + uiState: HomeUiState, + onGenderSelected: (GenderDivision) -> Unit, + onSportSelected: (SportSelection) -> Unit, + navigateToGameDetails: (String) -> Unit ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { item { @@ -121,12 +139,15 @@ private fun HomeContent( onSportSelected = onSportSelected, ) } + Box(modifier = Modifier.background(White)) { + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp), + color = GrayStroke, + ) + } } item { - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp, bottom = 24.dp), - color = GrayStroke, - ) + Spacer(modifier = Modifier.height(24.dp)) } items(uiState.filteredGames) { val game = it @@ -164,7 +185,8 @@ private fun HomeScreenPreview() = ScorePreview { loadedState = ApiResponse.Success(gameList) ), onGenderSelected = {}, - onSportSelected = {} + onSportSelected = {}, + onRefresh = {}, ) } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt index e23a8bb..af87dcc 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/PastGamesScreen.kt @@ -3,6 +3,7 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background 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.Spacer @@ -25,6 +26,7 @@ import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.LoadingScreen import com.cornellappdev.score.components.PastGameCard import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.components.ScorePullToRefreshBox import com.cornellappdev.score.components.SportSelectorHeader import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.GenderDivision @@ -65,7 +67,8 @@ fun PastGamesScreen( uiState = uiState, onGenderSelected = { pastGamesViewModel.onGenderSelected(it) }, onSportSelected = { pastGamesViewModel.onSportSelected(it) }, - navigateToGameDetails = navigateToGameDetails + navigateToGameDetails = navigateToGameDetails, + onRefresh = pastGamesViewModel::onRefresh ) } } @@ -78,7 +81,21 @@ private fun PastGamesContent( uiState: PastGamesUiState, onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, + onRefresh: () -> Unit, navigateToGameDetails: (String) -> Unit = {} +) { + ScorePullToRefreshBox(uiState.loadedState == ApiResponse.Loading, onRefresh = onRefresh) { + PastGamesLazyColumn(uiState, onGenderSelected, onSportSelected, navigateToGameDetails) + } +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun PastGamesLazyColumn( + uiState: PastGamesUiState, + onGenderSelected: (GenderDivision) -> Unit, + onSportSelected: (SportSelection) -> Unit, + navigateToGameDetails: (String) -> Unit ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { item { @@ -98,9 +115,11 @@ private fun PastGamesContent( GamesCarousel(uiState.pastGames, navigateToGameDetails) } stickyHeader { - Column(modifier = Modifier - .background(White) - .padding(horizontal = 24.dp)) { + Column( + modifier = Modifier + .background(White) + .padding(horizontal = 24.dp) + ) { Spacer(Modifier.height(24.dp)) Text( text = "All Scores", @@ -117,19 +136,22 @@ private fun PastGamesContent( onSportSelected = onSportSelected ) } + Box(modifier = Modifier.background(White)) { + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp), + color = GrayStroke, + ) + } } item { - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp, bottom = 24.dp), - color = GrayStroke, - ) + Spacer(modifier = Modifier.height(24.dp)) } items(uiState.filteredGames) { val game = it - Column (modifier = Modifier.padding(horizontal = 24.dp)) { + Column(modifier = Modifier.padding(horizontal = 24.dp)) { PastGameCard( data = game, - onClick = {navigateToGameDetails(game.id)} + onClick = { navigateToGameDetails(game.id) } ) Spacer(modifier = Modifier.height(16.dp)) } @@ -149,5 +171,6 @@ private fun PastGamesPreview() = ScorePreview { ), onGenderSelected = {}, onSportSelected = {}, + onRefresh = {}, ) } \ No newline at end of file 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 47084f1..1b69f30 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt @@ -17,17 +17,16 @@ data class GameDetailsUiState( @HiltViewModel class GameDetailsViewModel @Inject constructor( - scoreRepository: ScoreRepository, - savedStateHandle: SavedStateHandle + private val scoreRepository: ScoreRepository, + savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialUiState = GameDetailsUiState( loadedState = ApiResponse.Loading ) ) { - private val gameDetailsPageData = savedStateHandle.toRoute() + private val gameId: String = checkNotNull(savedStateHandle["gameId"]) init { - scoreRepository.getGameById(gameDetailsPageData.gameId) asyncCollect(scoreRepository.currentGamesFlow) { response -> applyMutation { copy( @@ -37,5 +36,11 @@ class GameDetailsViewModel @Inject constructor( ) } } + onRefresh() + } + + fun onRefresh() { + applyMutation { copy(loadedState = ApiResponse.Loading) } + scoreRepository.getGameById(gameId) } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt index 15f657d..023b90b 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/PastGamesViewModel.kt @@ -58,15 +58,7 @@ class PastGamesViewModel @Inject constructor( } } } - - fun onRefresh() { - applyMutation { - copy(loadedState = ApiResponse.Loading) - } - - scoreRepository.fetchGames() - } - + fun onGenderSelected(gender: GenderDivision) { applyMutation { copy( @@ -83,4 +75,10 @@ class PastGamesViewModel @Inject constructor( ) } } + + fun onRefresh() { + applyMutation { copy(loadedState = ApiResponse.Loading) } + + scoreRepository.fetchGames() + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f72f8bd..0fa6263 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,9 @@ constraintlayout = "2.1.4" runtimeAndroid = "1.7.2" apollo = "4.1.1" media3CommonKtx = "1.5.1" -material3 = "1.3.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" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }