diff --git a/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/BottomSheetGraph.kt b/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/BottomSheetGraph.kt index 6e49c97b5..e3cef0673 100644 --- a/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/BottomSheetGraph.kt +++ b/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/BottomSheetGraph.kt @@ -8,6 +8,7 @@ import ca.josephroque.bowlingcompanion.core.navigation.popBackStackWithResult import ca.josephroque.bowlingcompanion.feature.accessoriesoverview.navigation.accessoriesOnboardingSheet import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.gamesSettingsScreen import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.navigateToGamesEditor +import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.scoreEditorScreen import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.scoresListScreen import ca.josephroque.bowlingcompanion.feature.matchplayeditor.navigation.matchPlayEditorScreen import ca.josephroque.bowlingcompanion.feature.overview.navigation.navigateToQuickPlayOnboarding @@ -111,4 +112,7 @@ fun NavGraphBuilder.bottomSheetGraph(navController: NavController) { ) }, ) + scoreEditorScreen( + onDismissWithResult = navController::popBackStackWithResult, + ) } diff --git a/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/OverviewGraph.kt b/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/OverviewGraph.kt index 47a7a90c8..27df7e994 100644 --- a/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/OverviewGraph.kt +++ b/android/app/src/main/java/ca/josephroque/bowlingcompanion/navigation/graph/OverviewGraph.kt @@ -16,6 +16,7 @@ import ca.josephroque.bowlingcompanion.feature.bowlerform.navigation.navigateToN import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.gamesEditorScreen import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.navigateToGamesEditor import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.navigateToGamesSettingsForResult +import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.navigateToScoreEditorForResult import ca.josephroque.bowlingcompanion.feature.gameseditor.navigation.navigateToScoresList import ca.josephroque.bowlingcompanion.feature.laneform.navigation.laneFormScreen import ca.josephroque.bowlingcompanion.feature.leaguedetails.navigation.leagueDetailsScreen @@ -172,6 +173,13 @@ fun NavGraphBuilder.overviewGraph( }, onShowStatistics = { args -> navController.navigateToMidGameStatisticsDetails(args.filter) }, onShowBowlerScores = { args -> navController.navigateToScoresList(args.gameIndex, args.series) }, + onEditScore = { args -> + navController.navigateToScoreEditorForResult( + args.scoringMethod, + args.score, + args.onScoreUpdated, + ) + }, ) statisticsWidgetLayoutEditorScreen( onBackPressed = navController::popBackStack, diff --git a/android/core/navigation/src/main/java/ca/josephroque/bowlingcompanion/core/navigation/Route.kt b/android/core/navigation/src/main/java/ca/josephroque/bowlingcompanion/core/navigation/Route.kt index d143b19fb..6e64aa566 100644 --- a/android/core/navigation/src/main/java/ca/josephroque/bowlingcompanion/core/navigation/Route.kt +++ b/android/core/navigation/src/main/java/ca/josephroque/bowlingcompanion/core/navigation/Route.kt @@ -3,6 +3,7 @@ package ca.josephroque.bowlingcompanion.core.navigation import android.net.Uri import androidx.lifecycle.SavedStateHandle import ca.josephroque.bowlingcompanion.core.model.BowlerKind +import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod import ca.josephroque.bowlingcompanion.core.model.ResourcePickerType import ca.josephroque.bowlingcompanion.core.model.StatisticsDetailsSourceType import ca.josephroque.bowlingcompanion.core.statistics.StatisticID @@ -115,6 +116,15 @@ sealed class Route( fun getGameIndex(savedStateHandle: SavedStateHandle): Int? = savedStateHandle.get("gameIndex") } + data object ScoreEditor : Route("edit_score/{scoringMethod}/{score}", isBottomBarVisible = false) { + const val ARG_SCORING_METHOD = "scoringMethod" + const val ARG_SCORE = "score" + fun createRoute(scoringMethod: GameScoringMethod, score: Int): String = + "edit_score/${Uri.encode(scoringMethod.name)}/$score" + fun getScoringMethod(savedStateHandle: SavedStateHandle) = + savedStateHandle.get("scoringMethod") + fun getScore(savedStateHandle: SavedStateHandle) = savedStateHandle.get("score") + } // Gear data object GearList : Route("gear") diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreen.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreen.kt index 1fdeb8f4b..ec06ca596 100644 --- a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreen.kt +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreen.kt @@ -50,6 +50,7 @@ internal fun GamesEditorRoute( onShowGamesSettings: (GamesEditorArguments.ShowGamesSettings) -> Unit, onShowStatistics: (GamesEditorArguments.ShowStatistics) -> Unit, onShowBowlerScores: (GamesEditorArguments.ShowBowlerScores) -> Unit, + onEditScore: (GamesEditorArguments.EditScore) -> Unit, modifier: Modifier = Modifier, viewModel: GamesEditorViewModel = hiltViewModel(), ) { @@ -109,6 +110,12 @@ internal fun GamesEditorRoute( onShowStatistics(GamesEditorArguments.ShowStatistics(it.filter)) is GamesEditorScreenEvent.ShowBowlerScores -> onShowBowlerScores(GamesEditorArguments.ShowBowlerScores(it.series, it.gameIndex)) + is GamesEditorScreenEvent.EditScore -> + onEditScore( + GamesEditorArguments.EditScore(it.score, it.scoringMethod) { result -> + viewModel.handleAction(GamesEditorScreenUiAction.ScoreUpdated(result.second, result.first)) + }, + ) } } } diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreenUi.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreenUi.kt index abcdd6866..0044010e7 100644 --- a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreenUi.kt +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorScreenUi.kt @@ -1,5 +1,6 @@ package ca.josephroque.bowlingcompanion.feature.gameseditor +import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod import ca.josephroque.bowlingcompanion.core.model.TrackableFilter import ca.josephroque.bowlingcompanion.core.navigation.NavResultCallback import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.GamesEditorUiAction @@ -38,6 +39,12 @@ object GamesEditorArguments { val onSeriesUpdated: NavResultCallback, UUID>>, ) + data class EditScore( + val score: Int, + val scoringMethod: GameScoringMethod, + val onScoreUpdated: NavResultCallback>, + ) + data class ShowStatistics(val filter: TrackableFilter) data class ShowBowlerScores(val series: List, val gameIndex: Int) @@ -67,6 +74,10 @@ sealed interface GamesEditorScreenUiAction { data class SeriesUpdated(val series: List) : GamesEditorScreenUiAction data class CurrentGameUpdated(val gameId: UUID) : GamesEditorScreenUiAction data class SelectedBallUpdated(val ballId: UUID?) : GamesEditorScreenUiAction + data class ScoreUpdated( + val score: Int, + val scoringMethod: GameScoringMethod, + ) : GamesEditorScreenUiAction } sealed interface GamesEditorScreenEvent { @@ -81,6 +92,7 @@ sealed interface GamesEditorScreenEvent { val series: List, val currentGameId: UUID, ) : GamesEditorScreenEvent + data class EditScore(val score: Int, val scoringMethod: GameScoringMethod) : GamesEditorScreenEvent data class ShowStatistics(val filter: TrackableFilter) : GamesEditorScreenEvent data class ShowBowlerScores(val series: List, val gameIndex: Int) : GamesEditorScreenEvent } diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorViewModel.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorViewModel.kt index 3ab39bd99..2dfa08f9b 100644 --- a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorViewModel.kt +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/GamesEditorViewModel.kt @@ -21,7 +21,6 @@ import ca.josephroque.bowlingcompanion.core.data.repository.ScoresRepository import ca.josephroque.bowlingcompanion.core.data.repository.SeriesRepository import ca.josephroque.bowlingcompanion.core.model.ExcludeFromStatistics import ca.josephroque.bowlingcompanion.core.model.FrameEdit -import ca.josephroque.bowlingcompanion.core.model.Game import ca.josephroque.bowlingcompanion.core.model.GameLockState import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod import ca.josephroque.bowlingcompanion.core.model.GearKind @@ -40,10 +39,7 @@ import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.gamedetails.GameDe import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.gamedetails.GameDetailsUiState import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.gamedetails.NextGameEditableElement import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.rolleditor.RollEditorUiAction -import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiAction -import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiState import ca.josephroque.bowlingcompanion.feature.gameseditor.utils.ensureRollExists -import ca.josephroque.bowlingcompanion.feature.gameseditor.utils.getAndUpdateGamesEditor import ca.josephroque.bowlingcompanion.feature.gameseditor.utils.selectedFrame import ca.josephroque.bowlingcompanion.feature.gameseditor.utils.setBallRolled import ca.josephroque.bowlingcompanion.feature.gameseditor.utils.setDidFoul @@ -156,6 +152,7 @@ class GamesEditorViewModel @Inject constructor( is GamesEditorScreenUiAction.SeriesUpdated -> updateSeries(action.series) is GamesEditorScreenUiAction.CurrentGameUpdated -> loadGameIfChanged(action.gameId) is GamesEditorScreenUiAction.SelectedBallUpdated -> updateSelectedBall(id = action.ballId) + is GamesEditorScreenUiAction.ScoreUpdated -> updateScore(action.score, action.scoringMethod) } } @@ -186,7 +183,6 @@ class GamesEditorViewModel @Inject constructor( is GamesEditorUiAction.FrameEditor -> handleFrameEditorAction(action.action) is GamesEditorUiAction.RollEditor -> handleRollEditorAction(action.action) is GamesEditorUiAction.ScoreSheet -> handleScoreSheetAction(action.action) - is GamesEditorUiAction.ScoreEditor -> handleScoreEditorAction(action.action) } } @@ -213,17 +209,6 @@ class GamesEditorViewModel @Inject constructor( } } - private fun handleScoreEditorAction(action: ScoreEditorUiAction) { - when (action) { - ScoreEditorUiAction.CancelClicked -> dismissScoreEditor(didSave = false) - ScoreEditorUiAction.SaveClicked -> dismissScoreEditor(didSave = true) - is ScoreEditorUiAction.ScoreChanged -> updateScoreEditorScore(score = action.score) - is ScoreEditorUiAction.ScoringMethodChanged -> updateScoreEditorScoringMethod( - score = action.scoringMethod, - ) - } - } - private fun updateSeries(series: List) { this.series.update { series } } @@ -562,14 +547,12 @@ class GamesEditorViewModel @Inject constructor( isGameDetailsSheetVisible.value = false val gameDetails = gameDetailsState.value - gamesEditorState.updateGamesEditor(gameDetails.gameId) { - it.copy( - scoreEditor = ScoreEditorUiState( - score = gameDetails.scoringMethod.score, - scoringMethod = gameDetails.scoringMethod.scoringMethod, - ), - ) - } + sendEvent( + GamesEditorScreenEvent.EditScore( + score = gameDetails.scoringMethod.score, + scoringMethod = gameDetails.scoringMethod.scoringMethod, + ), + ) } private fun toggleGameLocked(isLocked: Boolean) { @@ -808,62 +791,41 @@ class GamesEditorViewModel @Inject constructor( saveFrame(gamesEditorState.selectedFrame()) } - private fun updateScoreEditorScore(score: String) { - gamesEditorState.update { - it.copy( - scoreEditor = it.scoreEditor?.copy( - score = score.toIntOrNull()?.coerceIn(0, Game.MAX_SCORE) ?: 0, - ), - ) - } - } - - private fun updateScoreEditorScoringMethod(score: GameScoringMethod) { - gamesEditorState.update { - it.copy( - scoreEditor = it.scoreEditor?.copy( - scoringMethod = score, - ), - ) + private fun updateScore(score: Int, scoringMethod: GameScoringMethod) { + if (isGameLocked) { + notifyGameLocked() + return } - } - private fun dismissScoreEditor(didSave: Boolean) { val gameId = currentGameId.value - val gamesEditorState = gamesEditorState.getAndUpdateGamesEditor(gameId) { - it.copy(scoreEditor = null) - } - val scoreEditor = gamesEditorState.scoreEditor ?: return - if (didSave && !isGameLocked) { - when (scoreEditor.scoringMethod) { + if (scoringMethod != gameDetailsState.value.scoringMethod.scoringMethod) { + when (scoringMethod) { GameScoringMethod.MANUAL -> analyticsClient.trackEvent(GameManualScoreSet(eventId = gameId)) GameScoringMethod.BY_FRAME -> Unit } + } - this.gamesEditorState.updateGamesEditor(gameId) { - it.copy( - manualScore = when (scoreEditor.scoringMethod) { - GameScoringMethod.MANUAL -> scoreEditor.score - GameScoringMethod.BY_FRAME -> null - }, - ) - } + gameDetailsState.updateGameDetails(gameId) { + it.copy( + scoringMethod = it.scoringMethod.copy( + score = score, + scoringMethod = scoringMethod, + ), + ) + } - viewModelScope.launch { - val score = when (scoreEditor.scoringMethod) { - GameScoringMethod.MANUAL -> scoreEditor.score - GameScoringMethod.BY_FRAME -> scoresRepository.getScore( - gamesEditorState.gameId, - ).first().score ?: 0 - } + gamesEditorState.updateGamesEditor(gameId) { + it.copy( + manualScore = when (scoringMethod) { + GameScoringMethod.MANUAL -> score + GameScoringMethod.BY_FRAME -> null + }, + ) + } - gamesRepository.setGameScoringMethod( - gamesEditorState.gameId, - scoreEditor.scoringMethod, - score, - ) - } + viewModelScope.launch { + gamesRepository.setGameScoringMethod(gameId, scoringMethod, score) } } diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/GamesEditorNavigation.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/GamesEditorNavigation.kt index 818510e00..d8fb56137 100644 --- a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/GamesEditorNavigation.kt +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/GamesEditorNavigation.kt @@ -29,6 +29,7 @@ fun NavGraphBuilder.gamesEditorScreen( onShowGamesSettings: (GamesEditorArguments.ShowGamesSettings) -> Unit, onShowStatistics: (GamesEditorArguments.ShowStatistics) -> Unit, onShowBowlerScores: (GamesEditorArguments.ShowBowlerScores) -> Unit, + onEditScore: (GamesEditorArguments.EditScore) -> Unit, ) { composable( route = Route.EditGame.route, @@ -47,6 +48,7 @@ fun NavGraphBuilder.gamesEditorScreen( onEditRolledBall = onEditRolledBall, onShowStatistics = onShowStatistics, onShowBowlerScores = onShowBowlerScores, + onEditScore = onEditScore, ) } } diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/ScoreEditorNavigation.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/ScoreEditorNavigation.kt new file mode 100644 index 000000000..fa256ced7 --- /dev/null +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/navigation/ScoreEditorNavigation.kt @@ -0,0 +1,42 @@ +package ca.josephroque.bowlingcompanion.feature.gameseditor.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.navArgument +import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod +import ca.josephroque.bowlingcompanion.core.navigation.NavResultCallback +import ca.josephroque.bowlingcompanion.core.navigation.Route +import ca.josephroque.bowlingcompanion.core.navigation.navigateForResult +import ca.josephroque.bowlingcompanion.feature.gameseditor.scoreeditor.ScoreEditorRoute +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import com.google.accompanist.navigation.material.bottomSheet + +fun NavController.navigateToScoreEditorForResult( + scoringMethod: GameScoringMethod, + score: Int, + navResultCallback: NavResultCallback>, + navOptions: NavOptions? = null, +) { + this.navigateForResult( + route = Route.ScoreEditor.createRoute(scoringMethod, score), + navResultCallback = navResultCallback, + navOptions = navOptions, + ) +} + +@OptIn(ExperimentalMaterialNavigationApi::class) +fun NavGraphBuilder.scoreEditorScreen(onDismissWithResult: (Pair) -> Unit) { + bottomSheet( + route = Route.ScoreEditor.route, + arguments = listOf( + navArgument(Route.ScoreEditor.ARG_SCORING_METHOD) { + type = NavType.EnumType(GameScoringMethod::class.java) + }, + navArgument(Route.ScoreEditor.ARG_SCORE) { type = NavType.IntType }, + ), + ) { + ScoreEditorRoute(onDismissWithResult = onDismissWithResult) + } +} diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorScreen.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorScreen.kt new file mode 100644 index 000000000..42e17a0ed --- /dev/null +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorScreen.kt @@ -0,0 +1,70 @@ +package ca.josephroque.bowlingcompanion.feature.gameseditor.scoreeditor + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditor +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorTopBar +import kotlinx.coroutines.launch + +@Composable +internal fun ScoreEditorRoute( + onDismissWithResult: (Pair) -> Unit, + modifier: Modifier = Modifier, + viewModel: ScoreEditorViewModel = hiltViewModel(), +) { + val scoreEditorScreenState by viewModel.uiState.collectAsStateWithLifecycle() + + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(Unit) { + lifecycleOwner.lifecycleScope.launch { + viewModel.events + .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .collect { + when (it) { + is ScoreEditorScreenEvent.Dismissed -> onDismissWithResult( + it.scoringMethod to it.score, + ) + } + } + } + } + + ScoreEditorScreen( + state = scoreEditorScreenState, + onAction = viewModel::handleAction, + modifier = modifier, + ) +} + +@Composable +private fun ScoreEditorScreen( + state: ScoreEditorScreenUiState, + onAction: (ScoreEditorScreenUiAction) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + topBar = { + ScoreEditorTopBar(onAction = { onAction(ScoreEditorScreenUiAction.ScoreEditor(it)) }) + }, + ) { padding -> + when (state) { + ScoreEditorScreenUiState.Loading -> Unit + is ScoreEditorScreenUiState.Loaded -> ScoreEditor( + state = state.scoreEditor, + onAction = { onAction(ScoreEditorScreenUiAction.ScoreEditor(it)) }, + modifier = modifier.padding(padding), + ) + } + } +} diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorScreenUi.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorScreenUi.kt new file mode 100644 index 000000000..97ab8475a --- /dev/null +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorScreenUi.kt @@ -0,0 +1,21 @@ +package ca.josephroque.bowlingcompanion.feature.gameseditor.scoreeditor + +import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiAction +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiState + +sealed interface ScoreEditorScreenUiState { + data object Loading : ScoreEditorScreenUiState + + data class Loaded( + val scoreEditor: ScoreEditorUiState, + ) : ScoreEditorScreenUiState +} + +sealed interface ScoreEditorScreenUiAction { + data class ScoreEditor(val action: ScoreEditorUiAction) : ScoreEditorScreenUiAction +} + +sealed interface ScoreEditorScreenEvent { + data class Dismissed(val scoringMethod: GameScoringMethod, val score: Int) : ScoreEditorScreenEvent +} diff --git a/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorViewModel.kt b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorViewModel.kt new file mode 100644 index 000000000..2ef75cba0 --- /dev/null +++ b/android/feature/gameseditor/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/scoreeditor/ScoreEditorViewModel.kt @@ -0,0 +1,72 @@ +package ca.josephroque.bowlingcompanion.feature.gameseditor.scoreeditor + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import ca.josephroque.bowlingcompanion.core.common.viewmodel.ApproachViewModel +import ca.josephroque.bowlingcompanion.core.model.Game +import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod +import ca.josephroque.bowlingcompanion.core.navigation.Route +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiAction +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +@HiltViewModel +class ScoreEditorViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : ApproachViewModel() { + private val initialScore = Route.ScoreEditor.getScore(savedStateHandle) ?: 0 + private val initialScoringMethod = Route.ScoreEditor.getScoringMethod( + savedStateHandle, + ) ?: GameScoringMethod.BY_FRAME + + private val score = MutableStateFlow(initialScore) + private val scoringMethod: MutableStateFlow = + MutableStateFlow(initialScoringMethod) + + val uiState: StateFlow = combine( + score, + scoringMethod, + ) { score, scoringMethod -> + ScoreEditorScreenUiState.Loaded( + scoreEditor = ScoreEditorUiState( + score = score, + scoringMethod = scoringMethod, + ), + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = ScoreEditorScreenUiState.Loading, + ) + + fun handleAction(action: ScoreEditorScreenUiAction) { + when (action) { + is ScoreEditorScreenUiAction.ScoreEditor -> handleScoreEditorAction(action.action) + } + } + + private fun handleScoreEditorAction(action: ScoreEditorUiAction) { + when (action) { + ScoreEditorUiAction.SaveClicked -> + sendEvent(ScoreEditorScreenEvent.Dismissed(scoringMethod.value, score.value)) + ScoreEditorUiAction.BackClicked -> + sendEvent(ScoreEditorScreenEvent.Dismissed(initialScoringMethod, initialScore)) + is ScoreEditorUiAction.ScoreChanged -> updateScore(action.score) + is ScoreEditorUiAction.ScoringMethodChanged -> updateScoringMethod(action.scoringMethod) + } + } + + private fun updateScore(score: String) { + this.score.value = score.toIntOrNull()?.coerceIn(0..Game.MAX_SCORE) ?: 0 + } + + private fun updateScoringMethod(scoringMethod: GameScoringMethod) { + this.scoringMethod.value = scoringMethod + } +} diff --git a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditor.kt b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditor.kt index 5b23fba1b..2ce9b06f5 100644 --- a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditor.kt +++ b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditor.kt @@ -41,7 +41,6 @@ import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.frameeditor.Animat import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.frameeditor.FrameEditorUiState import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.rolleditor.RollEditor import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.rolleditor.RollEditorUiState -import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditor import java.util.UUID @Composable @@ -50,13 +49,6 @@ fun GamesEditor( onAction: (GamesEditorUiAction) -> Unit, modifier: Modifier = Modifier, ) { - state.scoreEditor?.let { scoreEditor -> - ScoreEditor( - state = scoreEditor, - onAction = { onAction(GamesEditorUiAction.ScoreEditor(it)) }, - ) - } - Column(modifier = modifier.fillMaxSize()) { Box(modifier = Modifier.weight(1f)) { BackgroundImage() @@ -320,7 +312,6 @@ private fun GamesEditorPreview() { ), ), ), - scoreEditor = null, gameId = UUID.randomUUID(), ), onAction = {}, diff --git a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditorUi.kt b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditorUi.kt index 64e54d22a..5e63e8bcc 100644 --- a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditorUi.kt +++ b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/GamesEditorUi.kt @@ -7,8 +7,6 @@ import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.frameeditor.FrameE import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.frameeditor.FrameEditorUiState import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.rolleditor.RollEditorUiAction import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.rolleditor.RollEditorUiState -import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiAction -import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor.ScoreEditorUiState import java.util.UUID data class GamesEditorUiState( @@ -18,7 +16,6 @@ data class GamesEditorUiState( val rollEditor: RollEditorUiState = RollEditorUiState(), val scoreSheet: ScoreSheetUiState = ScoreSheetUiState(), val manualScore: Int? = null, - val scoreEditor: ScoreEditorUiState? = null, ) sealed interface GamesEditorUiAction { @@ -29,5 +26,4 @@ sealed interface GamesEditorUiAction { data class FrameEditor(val action: FrameEditorUiAction) : GamesEditorUiAction data class RollEditor(val action: RollEditorUiAction) : GamesEditorUiAction data class ScoreSheet(val action: ScoreSheetUiAction) : GamesEditorUiAction - data class ScoreEditor(val action: ScoreEditorUiAction) : GamesEditorUiAction } diff --git a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditor.kt b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditor.kt index da0f4d388..af58030f2 100644 --- a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditor.kt +++ b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditor.kt @@ -2,82 +2,61 @@ package ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import ca.josephroque.bowlingcompanion.core.designsystem.components.form.FormRadioGroup import ca.josephroque.bowlingcompanion.core.model.GameScoringMethod import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.R @Composable -fun ScoreEditor(state: ScoreEditorUiState, onAction: (ScoreEditorUiAction) -> Unit) { - Dialog( - onDismissRequest = { onAction(ScoreEditorUiAction.CancelClicked) }, +fun ScoreEditor( + state: ScoreEditorUiState, + onAction: (ScoreEditorUiAction) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .verticalScroll(rememberScrollState()) + .imePadding(), ) { - Surface( - shape = MaterialTheme.shapes.medium, - ) { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - FormRadioGroup( - title = stringResource(R.string.game_editor_scoring_method_title), - subtitle = stringResource(R.string.game_editor_scoring_method_subtitle), - options = GameScoringMethod.entries.toTypedArray(), - selected = state.scoringMethod, - titleForOption = { - when (it) { - GameScoringMethod.MANUAL -> stringResource(R.string.scoring_method_manual) - GameScoringMethod.BY_FRAME -> stringResource(R.string.scoring_method_frame_by_frame) - null -> "" - } - }, - onOptionSelected = { - it ?: return@FormRadioGroup - onAction(ScoreEditorUiAction.ScoringMethodChanged(it)) - }, - ) - - when (state.scoringMethod) { - GameScoringMethod.BY_FRAME -> Unit - GameScoringMethod.MANUAL -> { - ScoreTextField( - score = state.score, - onScoreChanged = { onAction(ScoreEditorUiAction.ScoreChanged(it)) }, - ) - } + FormRadioGroup( + title = stringResource(R.string.game_editor_scoring_method_title), + subtitle = stringResource(R.string.game_editor_scoring_method_subtitle), + options = GameScoringMethod.entries.toTypedArray(), + selected = state.scoringMethod, + titleForOption = { + when (it) { + GameScoringMethod.MANUAL -> stringResource(R.string.scoring_method_manual) + GameScoringMethod.BY_FRAME -> stringResource(R.string.scoring_method_frame_by_frame) + null -> "" } + }, + onOptionSelected = { + it ?: return@FormRadioGroup + onAction(ScoreEditorUiAction.ScoringMethodChanged(it)) + }, + ) - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Spacer(modifier = Modifier.weight(1f)) - - TextButton(onClick = { onAction(ScoreEditorUiAction.CancelClicked) }) { - Text(stringResource(ca.josephroque.bowlingcompanion.core.designsystem.R.string.action_cancel)) - } - - TextButton(onClick = { onAction(ScoreEditorUiAction.SaveClicked) }) { - Text(stringResource(ca.josephroque.bowlingcompanion.core.designsystem.R.string.action_save)) - } - } + when (state.scoringMethod) { + GameScoringMethod.BY_FRAME -> Unit + GameScoringMethod.MANUAL -> { + ScoreTextField( + score = state.score, + onScoreChanged = { onAction(ScoreEditorUiAction.ScoreChanged(it)) }, + ) } } } diff --git a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorTopBar.kt b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorTopBar.kt new file mode 100644 index 000000000..c1bf7b78e --- /dev/null +++ b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorTopBar.kt @@ -0,0 +1,34 @@ +package ca.josephroque.bowlingcompanion.feature.gameseditor.ui.scoreeditor + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ca.josephroque.bowlingcompanion.core.designsystem.components.CloseButton +import ca.josephroque.bowlingcompanion.feature.gameseditor.ui.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScoreEditorTopBar(onAction: (ScoreEditorUiAction) -> Unit) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.score_editor_title), + ) + }, + navigationIcon = { + CloseButton { onAction(ScoreEditorUiAction.BackClicked) } + }, + actions = { + TextButton(onClick = { onAction(ScoreEditorUiAction.SaveClicked) }) { + Text( + text = stringResource(ca.josephroque.bowlingcompanion.core.designsystem.R.string.action_save), + style = MaterialTheme.typography.bodyMedium, + ) + } + }, + ) +} diff --git a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorUi.kt b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorUi.kt index dede79186..8fe337566 100644 --- a/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorUi.kt +++ b/android/feature/gameseditor/ui/src/main/java/ca/josephroque/bowlingcompanion/feature/gameseditor/ui/scoreeditor/ScoreEditorUi.kt @@ -9,7 +9,7 @@ data class ScoreEditorUiState( sealed interface ScoreEditorUiAction { data object SaveClicked : ScoreEditorUiAction - data object CancelClicked : ScoreEditorUiAction + data object BackClicked : ScoreEditorUiAction data class ScoreChanged(val score: String) : ScoreEditorUiAction data class ScoringMethodChanged(val scoringMethod: GameScoringMethod) : ScoreEditorUiAction diff --git a/android/feature/gameseditor/ui/src/main/res/values/strings.xml b/android/feature/gameseditor/ui/src/main/res/values/strings.xml index b94ab8754..d4ceadf7b 100644 --- a/android/feature/gameseditor/ui/src/main/res/values/strings.xml +++ b/android/feature/gameseditor/ui/src/main/res/values/strings.xml @@ -58,6 +58,8 @@ Scores for Game %1$d + Scoring + Game Settings Games Bowlers