diff --git a/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt b/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt index 2ad242765..d8c2d10f1 100644 --- a/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt +++ b/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt @@ -3,7 +3,6 @@ package com.thomaskioko.tvmaniac.discover import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,8 +17,6 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -36,14 +33,23 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -55,7 +61,6 @@ import com.thomaskioko.tvmaniac.compose.components.BoxTextItems import com.thomaskioko.tvmaniac.compose.components.EmptyUi import com.thomaskioko.tvmaniac.compose.components.ErrorUi import com.thomaskioko.tvmaniac.compose.components.LoadingIndicator -import com.thomaskioko.tvmaniac.compose.components.RowError import com.thomaskioko.tvmaniac.compose.components.ThemePreviews import com.thomaskioko.tvmaniac.compose.components.TvManiacBackground import com.thomaskioko.tvmaniac.compose.components.TvPosterCard @@ -63,7 +68,6 @@ import com.thomaskioko.tvmaniac.compose.extensions.verticalGradientScrim import com.thomaskioko.tvmaniac.compose.theme.MinContrastOfPrimaryVsSurface import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme import com.thomaskioko.tvmaniac.compose.theme.contrastAgainst -import com.thomaskioko.tvmaniac.compose.util.DominantColorState import com.thomaskioko.tvmaniac.compose.util.DynamicThemePrimaryColorsFromImage import com.thomaskioko.tvmaniac.compose.util.rememberDominantColorState import com.thomaskioko.tvmaniac.navigation.extensions.viewModel @@ -71,7 +75,7 @@ import com.thomaskioko.tvmaniac.presentation.discover.DataLoaded import com.thomaskioko.tvmaniac.presentation.discover.DiscoverState import com.thomaskioko.tvmaniac.presentation.discover.Loading import com.thomaskioko.tvmaniac.presentation.discover.RetryLoading -import com.thomaskioko.tvmaniac.presentation.discover.ShowsAction +import com.thomaskioko.tvmaniac.presentation.discover.SnackBarDismissed import com.thomaskioko.tvmaniac.presentation.discover.model.TvShow import com.thomaskioko.tvmaniac.resources.R import dev.chrisbanes.snapper.ExperimentalSnapperApi @@ -110,26 +114,28 @@ internal fun DiscoverScreen( onMoreClicked: (showType: Long) -> Unit, ) { val discoverViewState by viewModel.state.collectAsStateWithLifecycle() + val pagerState = rememberPagerState() + val snackBarHostState = remember { SnackbarHostState() } DiscoverScreen( + modifier = modifier, state = discoverViewState, + snackBarHostState = snackBarHostState, + pagerState = pagerState, onShowClicked = onShowClicked, - onReloadClicked = { viewModel.dispatch(it) }, - onRetry = { viewModel.dispatch(RetryLoading) }, onMoreClicked = onMoreClicked, - modifier = modifier - .fillMaxSize() - .imePadding() - .navigationBarsPadding() - .animateContentSize(), + onRetry = { viewModel.dispatch(RetryLoading) }, + onErrorDismissed = { viewModel.dispatch(SnackBarDismissed) }, ) } @Composable private fun DiscoverScreen( state: DiscoverState, + snackBarHostState: SnackbarHostState, + pagerState: PagerState, onShowClicked: (showId: Long) -> Unit, - onReloadClicked: (ShowsAction) -> Unit, + onErrorDismissed: () -> Unit, modifier: Modifier = Modifier, onRetry: () -> Unit = {}, onMoreClicked: (showType: Long) -> Unit, @@ -144,18 +150,18 @@ private fun DiscoverScreen( is DataLoaded -> when { - state.isContentEmpty -> { - EmptyUi( + state.isContentEmpty && state.errorMessage != null -> { + ErrorUi( + errorMessage = state.errorMessage, + onRetry = onRetry, modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center), ) } - state.isContentEmpty && state.errorMessage != null -> { - ErrorUi( - errorMessage = state.errorMessage, - onRetry = onRetry, + state.isContentEmpty -> { + EmptyUi( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center), @@ -165,12 +171,16 @@ private fun DiscoverScreen( else -> { DiscoverScrollContent( modifier = modifier, + pagerState = pagerState, + snackBarHostState = snackBarHostState, onShowClicked = onShowClicked, onMoreClicked = onMoreClicked, + onSnackBarErrorDismissed = onErrorDismissed, trendingShows = state.trendingShows, popularShows = state.popularShows, anticipatedShows = state.anticipatedShows, recommendedShows = state.recommendedShows, + errorMessage = state.errorMessage, ) } } @@ -183,96 +193,108 @@ private fun DiscoverScrollContent( popularShows: ImmutableList?, anticipatedShows: ImmutableList?, recommendedShows: ImmutableList?, + errorMessage: String?, + snackBarHostState: SnackbarHostState, + pagerState: PagerState, + onSnackBarErrorDismissed: () -> Unit, onShowClicked: (showId: Long) -> Unit, modifier: Modifier = Modifier, onMoreClicked: (showType: Long) -> Unit, ) { - LazyColumn( - modifier = modifier - .fillMaxSize() - .windowInsetsPadding( - WindowInsets.systemBars.only(WindowInsetsSides.Horizontal), - ), - ) { - recommendedShows?.let { - item { - DiscoverHeaderContent( - showList = recommendedShows, - onShowClicked = onShowClicked, - ) + LaunchedEffect(key1 = errorMessage) { + errorMessage?.let { + val snackBarResult = snackBarHostState.showSnackbar( + message = errorMessage, + duration = SnackbarDuration.Short, + ) + when (snackBarResult) { + SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> + onSnackBarErrorDismissed() } } + } - trendingShows?.let { - item { - RowContent( - category = Category.TRENDING, - tvShows = trendingShows, - onItemClicked = onShowClicked, - onLabelClicked = onMoreClicked, - ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)), + ) { + recommendedShows?.let { + item { + DiscoverHeaderContent( + pagerState = pagerState, + showList = recommendedShows, + onShowClicked = onShowClicked, + ) + } } - } - anticipatedShows?.let { - item { - RowContent( - category = Category.ANTICIPATED, - tvShows = anticipatedShows, - onItemClicked = onShowClicked, - onLabelClicked = onMoreClicked, - ) + trendingShows?.let { + item { + RowContent( + category = Category.TRENDING, + tvShows = trendingShows, + onItemClicked = onShowClicked, + onLabelClicked = onMoreClicked, + ) + } } - } - popularShows?.let { - item { - RowContent( - category = Category.POPULAR, - tvShows = popularShows, - onItemClicked = onShowClicked, - onLabelClicked = onMoreClicked, - ) + anticipatedShows?.let { + item { + RowContent( + category = Category.ANTICIPATED, + tvShows = anticipatedShows, + onItemClicked = onShowClicked, + onLabelClicked = onMoreClicked, + ) + } + } + + popularShows?.let { + item { + RowContent( + category = Category.POPULAR, + tvShows = popularShows, + onItemClicked = onShowClicked, + onLabelClicked = onMoreClicked, + ) + } } } + + SnackbarHost(hostState = snackBarHostState) } } @Composable fun DiscoverHeaderContent( showList: ImmutableList, + pagerState: PagerState, modifier: Modifier = Modifier, onShowClicked: (Long) -> Unit, ) { - Column( - modifier = modifier - .windowInsetsPadding( - WindowInsets.systemBars.only(WindowInsetsSides.Horizontal), - ), - ) { - val surfaceColor = MaterialTheme.colorScheme.surface - val dominantColorState = rememberDominantColorState { color -> - // We want a color which has sufficient contrast against the surface color - color.contrastAgainst(surfaceColor) >= MinContrastOfPrimaryVsSurface - } + val selectedImageUrl = showList.getOrNull(pagerState.currentPage)?.posterImageUrl - DynamicThemePrimaryColorsFromImage(dominantColorState) { - val pagerState = rememberPagerState() - val selectedImageUrl = showList.getOrNull(pagerState.currentPage)?.posterImageUrl + DynamicColorContainer(selectedImageUrl) { + Column( + modifier = modifier + .windowInsetsPadding( + WindowInsets.systemBars.only(WindowInsetsSides.Horizontal), + ), + ) { + val backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - // When the selected image url changes, call updateColorsFromImageUrl() or reset() - LaunchedEffect(selectedImageUrl) { - if (selectedImageUrl != null) { - dominantColorState.updateColorsFromImageUrl(selectedImageUrl) - } else { - dominantColorState.reset() - } - } + DiscoverTopBar(backgroundColor = backgroundColor) HorizontalPagerItem( list = showList, pagerState = pagerState, - dominantColorState = dominantColorState, + backgroundColor = backgroundColor, onClick = onShowClicked, ) } @@ -281,29 +303,58 @@ fun DiscoverHeaderContent( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun HorizontalPagerItem( - list: List, - pagerState: PagerState, - dominantColorState: DominantColorState, - modifier: Modifier = Modifier, - onClick: (Long) -> Unit, +private fun DiscoverTopBar( + backgroundColor: Color, ) { - val selectedImageUrl = list.getOrNull(pagerState.currentPage)?.posterImageUrl + TopAppBar( + title = { }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = backgroundColor, + ), + modifier = Modifier.fillMaxWidth(), + ) +} - LaunchedEffect(selectedImageUrl) { - if (selectedImageUrl != null) { - dominantColorState.updateColorsFromImageUrl(selectedImageUrl) - } else { - dominantColorState.reset() +@Composable +private fun DynamicColorContainer( + selectedImageUrl: String?, + content: @Composable () -> Unit, +) { + val surfaceColor = MaterialTheme.colorScheme.surface + val dominantColorState = rememberDominantColorState { color -> + // We want a color which has sufficient contrast against the surface color + color.contrastAgainst(surfaceColor) >= MinContrastOfPrimaryVsSurface + } + + DynamicThemePrimaryColorsFromImage(dominantColorState) { + // When the selected image url changes, call updateColorsFromImageUrl() or reset() + LaunchedEffect(selectedImageUrl) { + if (selectedImageUrl != null) { + dominantColorState.updateColorsFromImageUrl(selectedImageUrl) + } else { + dominantColorState.reset() + } } + + content() } +} +@Composable +fun HorizontalPagerItem( + list: ImmutableList, + pagerState: PagerState, + backgroundColor: Color, + modifier: Modifier = Modifier, + onClick: (Long) -> Unit, +) { Column( modifier = modifier .fillMaxWidth() .verticalGradientScrim( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f), + color = backgroundColor, startYPercentage = 1f, endYPercentage = 0.5f, ), @@ -388,7 +439,7 @@ fun HorizontalPagerItem( @Composable private fun RowContent( category: Category, - tvShows: List, + tvShows: ImmutableList, onItemClicked: (Long) -> Unit, onLabelClicked: (Long) -> Unit, ) { @@ -423,24 +474,6 @@ private fun RowContent( } } -@Composable -private fun CategoryError( - categoryTitle: String, - onRetry: () -> Unit, - modifier: Modifier = Modifier, -) { - Column { - BoxTextItems( - title = categoryTitle, - ) - - RowError( - modifier = modifier, - onRetry = { onRetry() }, - ) - } -} - @ThemePreviews @Composable private fun DiscoverScreenPreview( @@ -450,11 +483,15 @@ private fun DiscoverScreenPreview( TvManiacTheme { TvManiacBackground { Surface(Modifier.fillMaxWidth()) { + val pagerState = rememberPagerState() + val snackBarHostState = remember { SnackbarHostState() } DiscoverScreen( state = state, + pagerState = pagerState, + snackBarHostState = snackBarHostState, onShowClicked = {}, onMoreClicked = {}, - onReloadClicked = {}, + onErrorDismissed = {}, ) } } diff --git a/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift b/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift index ddbbc4079..f9581f983 100644 --- a/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift +++ b/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift @@ -13,10 +13,17 @@ import TvManiac class DiscoverShowsViewModel: ObservableObject { private let stateMachine: DiscoverStateMachineWrapper = ApplicationComponentKt.discoverStateMachine() @Published private(set) var showState: DiscoverState = Loading() + @Published var toast: Toast? = nil func startStateMachine() { stateMachine.start(stateChangeListener: { (state: DiscoverState) -> Void in self.showState = state + if(state is DataLoaded){ + let dataLoaded = state as! DataLoaded + if(!dataLoaded.isContentEmpty && dataLoaded.errorMessage != nil){ + self.toast = Toast(type: .error, title: "Error", message: dataLoaded.errorMessage!) + } + } }) } } diff --git a/ios/ios/Feature/Discover/DiscoverView.swift b/ios/ios/Feature/Discover/DiscoverView.swift index 3de0e70bc..e0b2103b9 100644 --- a/ios/ios/Feature/Discover/DiscoverView.swift +++ b/ios/ios/Feature/Discover/DiscoverView.swift @@ -3,9 +3,9 @@ import TvManiac import os.log struct DiscoverView: View { - - @ObservedObject var viewModel: DiscoverShowsViewModel = DiscoverShowsViewModel() - + + @StateObject private var viewModel: DiscoverShowsViewModel = DiscoverShowsViewModel() + @Environment(\.colorScheme) var scheme @State var currentIndex: Int = 2 @@ -44,12 +44,10 @@ struct DiscoverView: View { case is DataLoaded: let state = contentState as! DataLoaded - if(state.isContentEmpty && state.errorMessage != nil){ + if(state.isContentEmpty && state.errorMessage != nil) { FullScreenView(systemName: "exclamationmark.triangle", message: state.errorMessage!) } else if(state.isContentEmpty){ FullScreenView(systemName: "list.and.film", message: "Looks like your stash is empty") - } else if(!state.isContentEmpty && state.errorMessage != nil){ - //TODO:: Show Toast } else { //Featured Shows @@ -84,7 +82,7 @@ struct DiscoverView: View { } } } - } + }.toastView(toast: $viewModel.toast) } diff --git a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsActions.kt b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsActions.kt index 2a1a98939..2d378ad6a 100644 --- a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsActions.kt +++ b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsActions.kt @@ -3,9 +3,6 @@ package com.thomaskioko.tvmaniac.presentation.discover sealed interface ShowsAction object RetryLoading : ShowsAction -object ReloadFeatured : ShowsAction -object ReloadPopular : ShowsAction -object ReloadAnticipated : ShowsAction -object ReloadTrending : ShowsAction +object SnackBarDismissed : ShowsAction data class ReloadCategory(val categoryId: Int) : ShowsAction diff --git a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt index efbb9726b..dc8ebb7b5 100644 --- a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt +++ b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt @@ -52,6 +52,12 @@ class DiscoverStateMachine( on { _, state -> state.override { Loading } } + + on { _, state -> + state.mutate { + copy(errorMessage = null) + } + } } } }