From 375cadb4e935a2df2a89eab416f2e83de8a83e21 Mon Sep 17 00:00:00 2001 From: joaomanaia Date: Sun, 22 Sep 2024 12:28:36 +0100 Subject: [PATCH] Add snackbar controller --- .../navigation/CompactNavigationContainer.kt | 9 ++++- .../navigation/ExpandedNavigationContainer.kt | 9 ++++- .../navigation/MediumNavigationContainer.kt | 7 ++++ .../ui/navigation/NavigationContainer.kt | 37 ++++++++++++++++++- .../ui/ComparisonQuizViewModel.kt | 8 +++- .../newquiz/core/ui/ObserveAsEvents.kt | 27 ++++++++++++++ .../newquiz/core/ui/SnackbarController.kt | 26 +++++++++++++ .../SavedMultiChoiceQuestionsViewModel.kt | 8 ++++ 8 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/com/infinitepower/newquiz/core/ui/ObserveAsEvents.kt create mode 100644 core/src/main/java/com/infinitepower/newquiz/core/ui/SnackbarController.kt diff --git a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/CompactNavigationContainer.kt b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/CompactNavigationContainer.kt index d1b83b91..0feef9ca 100644 --- a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/CompactNavigationContainer.kt +++ b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/CompactNavigationContainer.kt @@ -14,6 +14,8 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -47,6 +49,7 @@ internal fun CompactContainer( selectedItem: NavigationItem.Item?, userDiamonds: UInt = 0u, drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), + snackbarHostState: SnackbarHostState, content: @Composable (PaddingValues) -> Unit ) { val scope = rememberCoroutineScope() @@ -76,6 +79,9 @@ internal fun CompactContainer( ) { Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, topBar = { CenterAlignedTopAppBar( title = { @@ -168,7 +174,8 @@ private fun CompactContainerPreview() { primaryItems = getPrimaryItems(), otherItems = otherItems, selectedItem = selectedItem, - userDiamonds = 100u + userDiamonds = 100u, + snackbarHostState = SnackbarHostState() ) } } diff --git a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/ExpandedNavigationContainer.kt b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/ExpandedNavigationContainer.kt index 36ebca89..3cedb13d 100644 --- a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/ExpandedNavigationContainer.kt +++ b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/ExpandedNavigationContainer.kt @@ -7,6 +7,8 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.PermanentNavigationDrawer import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -37,6 +39,7 @@ internal fun ExpandedContainer( otherItems: ImmutableList, selectedItem: NavigationItem.Item?, userDiamonds: UInt = 0u, + snackbarHostState: SnackbarHostState, content: @Composable (PaddingValues) -> Unit ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( @@ -66,6 +69,9 @@ internal fun ExpandedContainer( ) { Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, topBar = { CenterAlignedTopAppBar( title = { @@ -115,7 +121,8 @@ private fun MediumContainerPreview() { primaryItems = getPrimaryItems(), otherItems = otherItems, selectedItem = selectedItem, - userDiamonds = 100u + userDiamonds = 100u, + snackbarHostState = SnackbarHostState() ) } } diff --git a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/MediumNavigationContainer.kt b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/MediumNavigationContainer.kt index bb2abc87..f7effb33 100644 --- a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/MediumNavigationContainer.kt +++ b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/MediumNavigationContainer.kt @@ -16,6 +16,8 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -48,6 +50,7 @@ internal fun MediumContainer( selectedItem: NavigationItem.Item?, userDiamonds: UInt = 0u, drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), + snackbarHostState: SnackbarHostState, content: @Composable (PaddingValues) -> Unit ) { val scope = rememberCoroutineScope() @@ -106,6 +109,9 @@ internal fun MediumContainer( Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, topBar = { CenterAlignedTopAppBar( scrollBehavior = scrollBehavior, @@ -156,6 +162,7 @@ private fun MediumContainerPreview() { primaryItems = getPrimaryItems(), otherItems = otherItems, selectedItem = selectedItem, + snackbarHostState = SnackbarHostState() ) } } diff --git a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/NavigationContainer.kt b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/NavigationContainer.kt index b1f4ded2..549150ed 100644 --- a/app/src/main/java/com/infinitepower/newquiz/ui/navigation/NavigationContainer.kt +++ b/app/src/main/java/com/infinitepower/newquiz/ui/navigation/NavigationContainer.kt @@ -14,26 +14,33 @@ import androidx.compose.material.icons.rounded.Today import androidx.compose.material.icons.rounded.ViewModule import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavController import com.infinitepower.newquiz.comparison_quiz.destinations.ComparisonQuizListScreenDestination import com.infinitepower.newquiz.core.R import com.infinitepower.newquiz.core.navigation.NavDrawerBadgeItem import com.infinitepower.newquiz.core.navigation.NavigationItem import com.infinitepower.newquiz.core.navigation.ScreenType +import com.infinitepower.newquiz.core.ui.ObserveAsEvents +import com.infinitepower.newquiz.core.ui.SnackbarController import com.infinitepower.newquiz.feature.daily_challenge.destinations.DailyChallengeScreenDestination -import com.infinitepower.newquiz.feature.settings.destinations.SettingsScreenDestination import com.infinitepower.newquiz.feature.maze.destinations.MazeScreenDestination import com.infinitepower.newquiz.feature.profile.destinations.ProfileScreenDestination +import com.infinitepower.newquiz.feature.settings.destinations.SettingsScreenDestination import com.infinitepower.newquiz.multi_choice_quiz.destinations.MultiChoiceQuizListScreenDestination import com.infinitepower.newquiz.wordle.destinations.WordleListScreenDestination import com.ramcosta.composedestinations.spec.DestinationSpec import com.ramcosta.composedestinations.utils.currentDestinationAsState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch internal fun getPrimaryItems(): ImmutableList = persistentListOf( NavigationItem.Item( @@ -108,6 +115,8 @@ internal fun NavigationContainer( userDiamonds: UInt, content: @Composable (PaddingValues) -> Unit ) { + val scope = rememberCoroutineScope() + val destination by navController.currentDestinationAsState() val primaryItems = remember { getPrimaryItems() } @@ -125,6 +134,24 @@ internal fun NavigationContainer( selectedItem != null && selectedItem.screenType == ScreenType.NORMAL } + val snackbarHostState = remember { SnackbarHostState() } + ObserveAsEvents(flow = SnackbarController.events, snackbarHostState) { event -> + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + + val result = snackbarHostState.showSnackbar( + message = event.message, + actionLabel = event.action?.name, + withDismissAction = event.withDismissAction, + duration = event.duration + ) + + if (result == SnackbarResult.ActionPerformed) { + event.action?.action?.invoke() + } + } + } + if (navigationVisible) { when (windowWidthSize) { WindowWidthSizeClass.Compact -> CompactContainer( @@ -133,6 +160,7 @@ internal fun NavigationContainer( otherItems = otherItems, selectedItem = selectedItem, userDiamonds = userDiamonds, + snackbarHostState = snackbarHostState, content = content ) @@ -142,6 +170,7 @@ internal fun NavigationContainer( otherItems = otherItems, selectedItem = selectedItem, userDiamonds = userDiamonds, + snackbarHostState = snackbarHostState, content = content ) @@ -151,12 +180,16 @@ internal fun NavigationContainer( otherItems = otherItems, selectedItem = selectedItem, userDiamonds = userDiamonds, + snackbarHostState = snackbarHostState, content = content ) } } else { Scaffold( - content = content + content = content, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } ) } } diff --git a/comparison-quiz/src/main/java/com/infinitepower/newquiz/comparison_quiz/ui/ComparisonQuizViewModel.kt b/comparison-quiz/src/main/java/com/infinitepower/newquiz/comparison_quiz/ui/ComparisonQuizViewModel.kt index 2a0c6551..a1f3020f 100644 --- a/comparison-quiz/src/main/java/com/infinitepower/newquiz/comparison_quiz/ui/ComparisonQuizViewModel.kt +++ b/comparison-quiz/src/main/java/com/infinitepower/newquiz/comparison_quiz/ui/ComparisonQuizViewModel.kt @@ -7,6 +7,8 @@ import androidx.lifecycle.viewModelScope import androidx.work.WorkManager import com.infinitepower.newquiz.comparison_quiz.core.workers.ComparisonQuizEndGameWorker import com.infinitepower.newquiz.core.game.ComparisonQuizCore +import com.infinitepower.newquiz.core.ui.SnackbarController +import com.infinitepower.newquiz.core.ui.SnackbarEvent import com.infinitepower.newquiz.core.user_services.InsufficientDiamondsException import com.infinitepower.newquiz.core.user_services.UserService import com.infinitepower.newquiz.data.worker.UpdateGlobalEventDataWorker @@ -130,6 +132,7 @@ class ComparisonQuizViewModel @Inject constructor( comparisonQuizCore.onAnswerClicked(event.item) } } + is ComparisonQuizUiEvent.ShowSkipQuestionDialog -> getUserDiamonds() is ComparisonQuizUiEvent.DismissSkipQuestionDialog -> { _uiState.update { currentState -> @@ -139,13 +142,16 @@ class ComparisonQuizViewModel @Inject constructor( ) } } + is ComparisonQuizUiEvent.SkipQuestion -> { viewModelScope.launch { try { comparisonQuizCore.skip() } catch (e: InsufficientDiamondsException) { e.printStackTrace() - // TODO: Show error dialog + SnackbarController.sendEvent( + event = SnackbarEvent(message = "Insufficient diamonds") + ) } } } diff --git a/core/src/main/java/com/infinitepower/newquiz/core/ui/ObserveAsEvents.kt b/core/src/main/java/com/infinitepower/newquiz/core/ui/ObserveAsEvents.kt new file mode 100644 index 00000000..0a333895 --- /dev/null +++ b/core/src/main/java/com/infinitepower/newquiz/core/ui/ObserveAsEvents.kt @@ -0,0 +1,27 @@ +package com.infinitepower.newquiz.core.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +@Composable +fun ObserveAsEvents( + flow: Flow, + vararg keys: Any?, + onEvent: (T) -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(lifecycleOwner.lifecycle, *keys, flow) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + withContext(Dispatchers.Main.immediate) { + flow.collect(onEvent) + } + } + } +} diff --git a/core/src/main/java/com/infinitepower/newquiz/core/ui/SnackbarController.kt b/core/src/main/java/com/infinitepower/newquiz/core/ui/SnackbarController.kt new file mode 100644 index 00000000..893cf40c --- /dev/null +++ b/core/src/main/java/com/infinitepower/newquiz/core/ui/SnackbarController.kt @@ -0,0 +1,26 @@ +package com.infinitepower.newquiz.core.ui + +import androidx.compose.material3.SnackbarDuration +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +object SnackbarController { + private val _events = Channel() + val events = _events.receiveAsFlow() + + suspend fun sendEvent(event: SnackbarEvent) { + _events.send(event) + } +} + +data class SnackbarEvent( + val message: String, + val action: SnackbarAction? = null, + val withDismissAction: Boolean = false, + val duration: SnackbarDuration = if (action == null) SnackbarDuration.Short else SnackbarDuration.Indefinite +) + +data class SnackbarAction( + val name: String, + val action: () -> Unit +) diff --git a/multi-choice-quiz/src/main/java/com/infinitepower/newquiz/multi_choice_quiz/saved_questions/SavedMultiChoiceQuestionsViewModel.kt b/multi-choice-quiz/src/main/java/com/infinitepower/newquiz/multi_choice_quiz/saved_questions/SavedMultiChoiceQuestionsViewModel.kt index fda46770..292a1979 100644 --- a/multi-choice-quiz/src/main/java/com/infinitepower/newquiz/multi_choice_quiz/saved_questions/SavedMultiChoiceQuestionsViewModel.kt +++ b/multi-choice-quiz/src/main/java/com/infinitepower/newquiz/multi_choice_quiz/saved_questions/SavedMultiChoiceQuestionsViewModel.kt @@ -9,6 +9,8 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import com.infinitepower.newquiz.core.analytics.AnalyticsEvent import com.infinitepower.newquiz.core.analytics.AnalyticsHelper +import com.infinitepower.newquiz.core.ui.SnackbarController +import com.infinitepower.newquiz.core.ui.SnackbarEvent import com.infinitepower.newquiz.data.worker.multichoicequiz.DownloadMultiChoiceQuestionsWorker import com.infinitepower.newquiz.domain.repository.multi_choice_quiz.saved_questions.SavedMultiChoiceQuestionsRepository import com.infinitepower.newquiz.model.multi_choice_quiz.MultiChoiceQuestion @@ -108,6 +110,12 @@ class SavedMultiChoiceQuestionsViewModel @Inject constructor( workManager .getWorkInfoByIdFlow(downloadQuestionsRequest.id) .onEach { info -> + if (info.state == WorkInfo.State.SUCCEEDED) { + SnackbarController.sendEvent( + event = SnackbarEvent(message = "Downloaded successfully") + ) + } + _uiState.update { it.copy( downloadingQuestions = info.state == WorkInfo.State.RUNNING