From 38062a65c1c854bdb854356a9825ccc8877b33a9 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Sun, 22 Mar 2026 13:52:02 -0400 Subject: [PATCH 1/5] first iteration --- app/build.gradle.kts | 13 ++++ .../com/cornellappdev/score/model/Game.kt | 32 ++++++++++ .../score/model/SocketManager.kt | 62 +++++++++++++++++++ .../cornellappdev/score/model/SocketModels.kt | 37 +++++++++++ .../score/viewmodel/GameDetailsViewModel.kt | 27 +++++++- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/model/SocketManager.kt create mode 100644 app/src/main/java/com/cornellappdev/score/model/SocketModels.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69a5f61..8b31e8a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,6 +43,11 @@ android { "BASE_URL", "\"${secrets.getProperty("API_URL_DEV")}\"" ) + buildConfigField( + "String", + "SOCKET_URL", + "\"${secrets.getProperty("SOCKET_URL_DEV")}\"" + ) } release { buildConfigField( @@ -50,6 +55,11 @@ android { "BASE_URL", "\"${secrets.getProperty("API_URL_PROD")}\"" ) + buildConfigField( + "String", + "SOCKET_URL", + "\"${secrets.getProperty("SOCKET_URL_PROD")}\"" + ) isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -97,6 +107,9 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("io.socket:socket.io-client:2.1.1") { + exclude(group = "org.json", module = "json") + } 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") 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..66dd3f2 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -302,6 +302,38 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData { ) } +/** + * Merges a live socket update into the current DetailsCardData. + * Null fields in [update] leave existing values unchanged. + */ +fun DetailsCardData.applySocketUpdate(update: SocketGameUpdateData): DetailsCardData { + val newScoreBreakdown = update.scoreBreakdown ?: scoreBreakdown + val newGameData = if (update.scoreBreakdown != null) { + toGameData( + scoreBreakdown = newScoreBreakdown, + team1 = TeamBoxScore("Cornell"), + team2 = TeamBoxScore(opponent), + sport = sport + ) + } else gameData + + val newBoxScore: List = update.boxScore + ?.map { it?.toGameDetailsBoxScore() } + ?: boxScore + val newScoreEvents = if (update.boxScore != null) { + newBoxScore.filterNotNull().toScoreEvents(opponentLogo) + } else scoreEvent + + return copy( + homeScore = update.homeScore ?: homeScore, + oppScore = update.oppScore ?: oppScore, + scoreBreakdown = newScoreBreakdown, + gameData = newGameData, + boxScore = newBoxScore, + scoreEvent = newScoreEvents + ) +} + fun List.toScoreEvents(teamLogo: String): List { return this.mapIndexed { index, boxScore -> val teamName = boxScore.team ?: "" diff --git a/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt b/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt new file mode 100644 index 0000000..ad6019c --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt @@ -0,0 +1,62 @@ +package com.cornellappdev.score.model + +import android.util.Log +import com.cornellappdev.score.BuildConfig +import io.socket.client.IO +import io.socket.client.Socket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import org.json.JSONObject +import java.util.Collections +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG = "SocketManager" + +@Singleton +class SocketManager @Inject constructor(private val appScope: CoroutineScope) { + + private val _gameUpdateFlow = MutableSharedFlow(extraBufferCapacity = 16) + val gameUpdateFlow: SharedFlow = _gameUpdateFlow.asSharedFlow() + + private val activeSubscriptions: MutableSet = Collections.synchronizedSet(mutableSetOf()) + + private val json = Json { ignoreUnknownKeys = true; isLenient = true } + + private val socket: Socket by lazy { + val opts = IO.Options.builder() + .setTransports(arrayOf("websocket")) + .build() + IO.socket(BuildConfig.SOCKET_URL, opts).also { s -> + s.on(Socket.EVENT_CONNECT) { + Log.d(TAG, "Connected") + activeSubscriptions.forEach { id -> + s.emit("subscribe", JSONObject().put("gameId", id)) + } + } + s.on(Socket.EVENT_DISCONNECT) { args -> Log.d(TAG, "Disconnected: ${args.firstOrNull()}") } + s.on(Socket.EVENT_CONNECT_ERROR) { args -> Log.e(TAG, "Error: ${args.firstOrNull()}") } + s.on("game_update") { args -> + val raw = args.firstOrNull() as? JSONObject ?: return@on + runCatching { json.decodeFromString(raw.toString()) } + .onSuccess { appScope.launch { _gameUpdateFlow.emit(it) } } + .onFailure { Log.e(TAG, "Parse error: $it") } + } + s.connect() + } + } + + fun subscribe(gameId: String) { + activeSubscriptions.add(gameId) + socket.emit("subscribe", JSONObject().put("gameId", gameId)) + } + + fun unsubscribe(gameId: String) { + activeSubscriptions.remove(gameId) + socket.emit("unsubscribe", JSONObject().put("gameId", gameId)) + } +} diff --git a/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt b/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt new file mode 100644 index 0000000..b59f3e0 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt @@ -0,0 +1,37 @@ +package com.cornellappdev.score.model + +import kotlinx.serialization.Serializable + +@Serializable +data class SocketGameUpdateEnvelope( + val type: String, + val gameId: String, + val timestamp: String, + val data: SocketGameUpdateData +) + +@Serializable +data class SocketGameUpdateData( + val homeScore: Int? = null, + val oppScore: Int? = null, + val scoreBreakdown: List?>? = null, + val boxScore: List? = null +) + +@Serializable +data class SocketBoxScoreEntry( + val team: String? = null, + val period: String? = null, + val time: String? = null, + val description: String? = null, + val scorer: String? = null, + val assist: String? = null, + val scoreBy: String? = null, + val corScore: Int? = null, + val oppScore: Int? = null +) + +fun SocketBoxScoreEntry.toGameDetailsBoxScore() = GameDetailsBoxScore( + team = team, period = period, time = time, description = description, + scorer = scorer, assist = assist, scoreBy = scoreBy, corScore = corScore, oppScore = oppScore +) 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..455e760 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt @@ -1,14 +1,16 @@ package com.cornellappdev.score.viewmodel import androidx.lifecycle.SavedStateHandle -import androidx.navigation.toRoute +import androidx.lifecycle.viewModelScope import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.DetailsCardData import com.cornellappdev.score.model.ScoreRepository +import com.cornellappdev.score.model.SocketManager +import com.cornellappdev.score.model.applySocketUpdate import com.cornellappdev.score.model.map import com.cornellappdev.score.model.toGameCardData -import com.cornellappdev.score.nav.root.ScoreScreens import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject data class GameDetailsUiState( @@ -18,6 +20,7 @@ data class GameDetailsUiState( @HiltViewModel class GameDetailsViewModel @Inject constructor( private val scoreRepository: ScoreRepository, + private val socketManager: SocketManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialUiState = GameDetailsUiState( @@ -37,10 +40,28 @@ class GameDetailsViewModel @Inject constructor( } } onRefresh() + + socketManager.subscribe(gameId) + + viewModelScope.launch { + socketManager.gameUpdateFlow.collect { envelope -> + if (envelope.gameId != gameId) return@collect + applyMutation { + val current = loadedState + if (current !is ApiResponse.Success) return@applyMutation this + copy(loadedState = ApiResponse.Success(current.data.applySocketUpdate(envelope.data))) + } + } + } } fun onRefresh() { applyMutation { copy(loadedState = ApiResponse.Loading) } scoreRepository.getGameById(gameId) } -} \ No newline at end of file + + override fun onCleared() { + super.onCleared() + socketManager.unsubscribe(gameId) + } +} From 7f47b0977da17fbb08368132f42b9d650c677861 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 22 Apr 2026 17:19:03 -0400 Subject: [PATCH 2/5] slight mod --- .../com/cornellappdev/score/model/SocketManager.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt b/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt index ad6019c..ac6064d 100644 --- a/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt +++ b/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt @@ -20,10 +20,12 @@ private const val TAG = "SocketManager" @Singleton class SocketManager @Inject constructor(private val appScope: CoroutineScope) { - private val _gameUpdateFlow = MutableSharedFlow(extraBufferCapacity = 16) + private val _gameUpdateFlow = + MutableSharedFlow(extraBufferCapacity = 16) val gameUpdateFlow: SharedFlow = _gameUpdateFlow.asSharedFlow() - private val activeSubscriptions: MutableSet = Collections.synchronizedSet(mutableSetOf()) + private val activeSubscriptions: MutableSet = + Collections.synchronizedSet(mutableSetOf()) private val json = Json { ignoreUnknownKeys = true; isLenient = true } @@ -32,13 +34,19 @@ class SocketManager @Inject constructor(private val appScope: CoroutineScope) { .setTransports(arrayOf("websocket")) .build() IO.socket(BuildConfig.SOCKET_URL, opts).also { s -> + // "on" is a listener s.on(Socket.EVENT_CONNECT) { Log.d(TAG, "Connected") activeSubscriptions.forEach { id -> s.emit("subscribe", JSONObject().put("gameId", id)) } } - s.on(Socket.EVENT_DISCONNECT) { args -> Log.d(TAG, "Disconnected: ${args.firstOrNull()}") } + s.on(Socket.EVENT_DISCONNECT) { args -> + Log.d( + TAG, + "Disconnected: ${args.firstOrNull()}" + ) + } s.on(Socket.EVENT_CONNECT_ERROR) { args -> Log.e(TAG, "Error: ${args.firstOrNull()}") } s.on("game_update") { args -> val raw = args.firstOrNull() as? JSONObject ?: return@on From b1a706f8fe3dae9b5203ef68c35da982587a77d8 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 9 Sep 2026 16:07:21 -0400 Subject: [PATCH 3/5] Preserve game result when applying live score updates --- .../main/java/com/cornellappdev/score/model/Game.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 732343e..01f8c7a 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -127,7 +127,8 @@ data class DetailsCardData( val daysUntilGame: Int?, val hoursUntilGame: Int?, val homeScore: Int, - val oppScore: Int + val oppScore: Int, + val result: String = "" ) // Scoring information by round of a game, used in the box score @@ -304,7 +305,8 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData { homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport, result ?: "").second ?: parsedScores?.first ?: 0, oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport, result ?: "").second - ?: parsedScores?.second ?: 0 + ?: parsedScores?.second ?: 0, + result = result ?: "" ) } @@ -319,7 +321,8 @@ fun DetailsCardData.applySocketUpdate(update: SocketGameUpdateData): DetailsCard scoreBreakdown = newScoreBreakdown, team1 = TeamBoxScore("Cornell"), team2 = TeamBoxScore(opponent), - sport = sport + sport = sport, + result = result ) } else gameData @@ -360,4 +363,4 @@ fun List.toScoreEvents(teamLogo: String): List description = boxScore.description ) } -} \ No newline at end of file +} From 2403e9f07ddb4c82820ce3acfaad8d0ea0064824 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 23 Sep 2026 12:15:57 -0400 Subject: [PATCH 4/5] Fix live score updates and address review feedback --- .../com/cornellappdev/score/model/Game.kt | 39 +++++++++++-------- .../score/model/SocketManager.kt | 5 ++- .../cornellappdev/score/model/SocketModels.kt | 20 +--------- .../score/viewmodel/GameDetailsViewModel.kt | 28 ++++++++++--- 4 files changed, 50 insertions(+), 42 deletions(-) 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 01f8c7a..0366fb2 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -14,6 +14,8 @@ import kotlinx.serialization.Serializable import java.time.LocalDate import java.time.LocalDateTime +private const val CORNELL_TEAM_NAME = "Cornell" + // TODO Refactor to make easier to filter... actual gender, etc. data class Game( @@ -43,16 +45,17 @@ data class GameDetailsTeam( val name: String ) +@Serializable data class GameDetailsBoxScore( - val team: String?, - val period: String?, - val time: String?, - val description: String?, - val scorer: String?, - val assist: String?, - val scoreBy: String?, - val corScore: Int?, - val oppScore: Int? + val team: String? = null, + val period: String? = null, + val time: String? = null, + val description: String? = null, + val scorer: String? = null, + val assist: String? = null, + val scoreBy: String? = null, + val corScore: Int? = null, + val oppScore: Int? = null ) data class GameDetailsGame( @@ -273,7 +276,7 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData { val (daysUntil, hoursUntil) = getTimeUntilStart(date, time ?: "") ?: (null to null) val parsedScores = parseResultScore(result) return DetailsCardData( - title = "Cornell Vs. ${team?.name ?: ""}", + title = "$CORNELL_TEAM_NAME Vs. ${team?.name ?: ""}", opponentLogo = team?.image ?: "", opponent = team?.name ?: "", opponentColor = team?.color ?: Color.White, @@ -294,7 +297,7 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData { scoreBreakdown = scoreBreakdown ?: emptyList(), gameData = toGameData( scoreBreakdown = scoreBreakdown, - team1 = TeamBoxScore("Cornell"), + team1 = TeamBoxScore(CORNELL_TEAM_NAME), team2 = TeamBoxScore(team?.name ?: ""), sport = sport, result = result ?: "" @@ -319,23 +322,25 @@ fun DetailsCardData.applySocketUpdate(update: SocketGameUpdateData): DetailsCard val newGameData = if (update.scoreBreakdown != null) { toGameData( scoreBreakdown = newScoreBreakdown, - team1 = TeamBoxScore("Cornell"), + team1 = TeamBoxScore(CORNELL_TEAM_NAME), team2 = TeamBoxScore(opponent), sport = sport, result = result ) } else gameData - val newBoxScore: List = update.boxScore - ?.map { it?.toGameDetailsBoxScore() } - ?: boxScore + val newBoxScore = update.boxScore ?: boxScore val newScoreEvents = if (update.boxScore != null) { newBoxScore.filterNotNull().toScoreEvents(opponentLogo) } else scoreEvent return copy( - homeScore = update.homeScore ?: homeScore, - oppScore = update.oppScore ?: oppScore, + homeScore = update.homeScore + ?: update.scoreBreakdown?.let { convertScores(it.getOrNull(0), sport, result).second } + ?: homeScore, + oppScore = update.oppScore + ?: update.scoreBreakdown?.let { convertScores(it.getOrNull(1), sport, result).second } + ?: oppScore, scoreBreakdown = newScoreBreakdown, gameData = newGameData, boxScore = newBoxScore, diff --git a/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt b/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt index ac6064d..a554b8c 100644 --- a/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt +++ b/app/src/main/java/com/cornellappdev/score/model/SocketManager.kt @@ -37,7 +37,10 @@ class SocketManager @Inject constructor(private val appScope: CoroutineScope) { // "on" is a listener s.on(Socket.EVENT_CONNECT) { Log.d(TAG, "Connected") - activeSubscriptions.forEach { id -> + val subscriptions = synchronized(activeSubscriptions) { + activeSubscriptions.toList() + } + subscriptions.forEach { id -> s.emit("subscribe", JSONObject().put("gameId", id)) } } diff --git a/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt b/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt index b59f3e0..8355ec6 100644 --- a/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt +++ b/app/src/main/java/com/cornellappdev/score/model/SocketModels.kt @@ -15,23 +15,5 @@ data class SocketGameUpdateData( val homeScore: Int? = null, val oppScore: Int? = null, val scoreBreakdown: List?>? = null, - val boxScore: List? = null -) - -@Serializable -data class SocketBoxScoreEntry( - val team: String? = null, - val period: String? = null, - val time: String? = null, - val description: String? = null, - val scorer: String? = null, - val assist: String? = null, - val scoreBy: String? = null, - val corScore: Int? = null, - val oppScore: Int? = null -) - -fun SocketBoxScoreEntry.toGameDetailsBoxScore() = GameDetailsBoxScore( - team = team, period = period, time = time, description = description, - scorer = scorer, assist = assist, scoreBy = scoreBy, corScore = corScore, oppScore = oppScore + val boxScore: List? = null ) 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 455e760..85f54b0 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt @@ -5,12 +5,15 @@ import androidx.lifecycle.viewModelScope import com.cornellappdev.score.model.ApiResponse import com.cornellappdev.score.model.DetailsCardData import com.cornellappdev.score.model.ScoreRepository +import com.cornellappdev.score.model.SocketGameUpdateData import com.cornellappdev.score.model.SocketManager import com.cornellappdev.score.model.applySocketUpdate import com.cornellappdev.score.model.map import com.cornellappdev.score.model.toGameCardData import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch +import java.time.Instant import javax.inject.Inject data class GameDetailsUiState( @@ -28,31 +31,46 @@ class GameDetailsViewModel @Inject constructor( ) ) { private val gameId: String = checkNotNull(savedStateHandle["gameId"]) + private val pendingUpdates = mutableListOf() + private var latestUpdateTime: Instant? = null init { asyncCollect(scoreRepository.currentGamesFlow) { response -> + if (response is ApiResponse.Success && response.data.id != gameId) return@asyncCollect applyMutation { copy( loadedState = response.map { gameCard -> - gameCard.toGameCardData() + // Apply every partial update in order so omitted fields aren't lost. + pendingUpdates.fold(gameCard.toGameCardData()) { card, update -> + card.applySocketUpdate(update) + }.also { pendingUpdates.clear() } } ) } } onRefresh() - socketManager.subscribe(gameId) - - viewModelScope.launch { + // Start listening before subscribing, including an immediate subscription snapshot. + viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) { socketManager.gameUpdateFlow.collect { envelope -> if (envelope.gameId != gameId) return@collect + val updateTime = runCatching { Instant.parse(envelope.timestamp) }.getOrNull() + val latestTime = latestUpdateTime + if (updateTime != null && latestTime != null && updateTime.isBefore(latestTime)) { + return@collect + } + if (updateTime != null) latestUpdateTime = updateTime applyMutation { val current = loadedState - if (current !is ApiResponse.Success) return@applyMutation this + if (current !is ApiResponse.Success) { + pendingUpdates.add(envelope.data) + return@applyMutation this + } copy(loadedState = ApiResponse.Success(current.data.applySocketUpdate(envelope.data))) } } } + socketManager.subscribe(gameId) } fun onRefresh() { From 6c2aec9f6398b75831c932a54a1a1476129317fa Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 30 Sep 2026 13:59:45 -0400 Subject: [PATCH 5/5] Reuse Cornell team name constant across the app --- .../java/com/cornellappdev/score/components/PastGameCard.kt | 5 +++-- .../java/com/cornellappdev/score/components/ScoreSummary.kt | 5 +++-- app/src/main/java/com/cornellappdev/score/model/Game.kt | 3 +-- .../main/java/com/cornellappdev/score/util/CalendarUtil.kt | 2 +- .../main/java/com/cornellappdev/score/util/TeamConstants.kt | 3 +++ .../java/com/cornellappdev/score/util/TestingConstants.kt | 4 ++-- 6 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/util/TeamConstants.kt diff --git a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt index 16d09ef..134dd8b 100644 --- a/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/PastGameCard.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.cornellappdev.score.util.CORNELL_TEAM_NAME import com.cornellappdev.score.R import com.cornellappdev.score.model.GameCardData import com.cornellappdev.score.theme.AmbientColor @@ -155,7 +156,7 @@ private fun TeamScore( if (isCornell) { Image( painter = painterResource(R.drawable.cornell_logo), - contentDescription = "Cornell Logo", + contentDescription = "$CORNELL_TEAM_NAME Logo", modifier = Modifier .height(27.dp) .padding(horizontal = 2.dp, vertical = 4.dp) @@ -171,7 +172,7 @@ private fun TeamScore( } Spacer(modifier = Modifier.width(4.dp)) Text( - text = if (isCornell) "Cornell" else team, + text = if (isCornell) CORNELL_TEAM_NAME else team, style = heading2, color = if (winningTeam) GrayPrimary else GrayLight ) 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 68b339c..0b0162d 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ScoreSummary.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.cornellappdev.score.util.CORNELL_TEAM_NAME import com.cornellappdev.score.R import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.model.ScoreEvent @@ -100,7 +101,7 @@ fun ScoreEventItem(event: ScoreEvent) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = homeScore.toString(), - style = if (event.team.name == "Cornell") metricSemibold else metricNormal, // TODO: Check name + style = if (event.team.name == CORNELL_TEAM_NAME) metricSemibold else metricNormal, // TODO: Check name color = GrayPrimary, textAlign = TextAlign.Center ) @@ -112,7 +113,7 @@ fun ScoreEventItem(event: ScoreEvent) { ) Text( text = awayScore.toString(), - style = if (event.team.name != "Cornell") metricSemibold else metricNormal, + style = if (event.team.name != CORNELL_TEAM_NAME) metricSemibold else metricNormal, color = GrayPrimary, textAlign = TextAlign.Center ) 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 0366fb2..672490c 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -1,6 +1,7 @@ package com.cornellappdev.score.model import androidx.compose.ui.graphics.Color +import com.cornellappdev.score.util.CORNELL_TEAM_NAME import com.cornellappdev.score.R import com.cornellappdev.score.util.convertScores import com.cornellappdev.score.util.formatDateTimeDisplay @@ -14,8 +15,6 @@ import kotlinx.serialization.Serializable import java.time.LocalDate import java.time.LocalDateTime -private const val CORNELL_TEAM_NAME = "Cornell" - // TODO Refactor to make easier to filter... actual gender, etc. data class Game( diff --git a/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt b/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt index 24b02f1..288a498 100644 --- a/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt +++ b/app/src/main/java/com/cornellappdev/score/util/CalendarUtil.kt @@ -43,7 +43,7 @@ fun DetailsCardData.toCalendarEvent(): CalendarEvent? { val date = this.date ?: return null return CalendarEvent( - title = "Cornell vs. ${this.opponent}", + title = "$CORNELL_TEAM_NAME vs. ${this.opponent}", description = "${this.sport} game (${this.gender})", location = this.locationString, date = date, diff --git a/app/src/main/java/com/cornellappdev/score/util/TeamConstants.kt b/app/src/main/java/com/cornellappdev/score/util/TeamConstants.kt new file mode 100644 index 0000000..5942358 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/util/TeamConstants.kt @@ -0,0 +1,3 @@ +package com.cornellappdev.score.util + +const val CORNELL_TEAM_NAME = "Cornell" diff --git a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt index 476077b..a726a12 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -58,7 +58,7 @@ val gameList = listOf( PRINCETON_GAME ) -val team1 = TeamBoxScore(name = "Cornell") +val team1 = TeamBoxScore(name = CORNELL_TEAM_NAME) val team2 = TeamBoxScore(name = "Yale University") val teamScore1 = TeamScore( @@ -129,7 +129,7 @@ val longGameData = GameData(teamScores = longGameTeamScore1 to longGameTeamScore val extraLongGameData = GameData(teamScores = extraLongGameTeamScore1 to extraLongGameTeamScore2) val team3 = TeamGameSummary( - name = "Cornell", + name = CORNELL_TEAM_NAME, "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max" ) val team4 = TeamGameSummary(