|
|
@ -22,6 +22,7 @@ import androidx.compose.animation.fadeIn
|
|
|
|
import androidx.compose.animation.fadeOut
|
|
|
|
import androidx.compose.animation.fadeOut
|
|
|
|
import androidx.compose.animation.slideInVertically
|
|
|
|
import androidx.compose.animation.slideInVertically
|
|
|
|
import androidx.compose.animation.slideOutVertically
|
|
|
|
import androidx.compose.animation.slideOutVertically
|
|
|
|
|
|
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
@ -43,7 +44,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells.Adaptive
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells.Adaptive
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
|
|
import androidx.compose.foundation.lazy.grid.items
|
|
|
|
import androidx.compose.foundation.lazy.grid.items
|
|
|
@ -82,13 +83,10 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
|
|
|
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
|
|
|
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
|
|
|
|
import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
|
|
|
|
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
|
|
|
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.NewsItem
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@OptIn(ExperimentalLifecycleComposeApi::class)
|
|
|
|
@OptIn(ExperimentalLifecycleComposeApi::class)
|
|
|
|
@Composable
|
|
|
|
@Composable
|
|
|
@ -96,33 +94,38 @@ internal fun ForYouRoute(
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
viewModel: ForYouViewModel = hiltViewModel()
|
|
|
|
viewModel: ForYouViewModel = hiltViewModel()
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
val onboardingUiState by viewModel.onboardingUiState.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
|
|
|
|
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
val forYouItems by viewModel.forYouItems.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
val lazyGridState = rememberLazyGridState()
|
|
|
|
|
|
|
|
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
isSyncing = isSyncing,
|
|
|
|
isSyncing = isSyncing,
|
|
|
|
onboardingUiState = onboardingUiState,
|
|
|
|
forYouItems = forYouItems,
|
|
|
|
feedState = feedState,
|
|
|
|
|
|
|
|
onTopicCheckedChanged = viewModel::updateTopicSelection,
|
|
|
|
onTopicCheckedChanged = viewModel::updateTopicSelection,
|
|
|
|
saveFollowedTopics = viewModel::dismissOnboarding,
|
|
|
|
saveFollowedTopics = viewModel::dismissOnboarding,
|
|
|
|
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
|
|
|
|
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
|
|
|
|
modifier = modifier
|
|
|
|
modifier = modifier,
|
|
|
|
|
|
|
|
lazyGridState = lazyGridState
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
|
|
@Composable
|
|
|
|
@Composable
|
|
|
|
internal fun ForYouScreen(
|
|
|
|
internal fun ForYouScreen(
|
|
|
|
isSyncing: Boolean,
|
|
|
|
isSyncing: Boolean,
|
|
|
|
onboardingUiState: OnboardingUiState,
|
|
|
|
forYouItems: List<ForYouItem>,
|
|
|
|
feedState: NewsFeedUiState,
|
|
|
|
|
|
|
|
onTopicCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
onTopicCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
saveFollowedTopics: () -> Unit,
|
|
|
|
saveFollowedTopics: () -> Unit,
|
|
|
|
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
|
|
|
|
lazyGridState: LazyGridState = rememberLazyGridState(),
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
val isOnboardingLoading = onboardingUiState is OnboardingUiState.Loading
|
|
|
|
val isOnboardingLoading = forYouItems.any {
|
|
|
|
val isFeedLoading = feedState is NewsFeedUiState.Loading
|
|
|
|
it is ForYouItem.OnBoarding && it.onboardingUiState is OnboardingUiState.Loading
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
val isFeedLoading = forYouItems.any {
|
|
|
|
|
|
|
|
it is ForYouItem.News.Loading
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Workaround to call Activity.reportFullyDrawn from Jetpack Compose.
|
|
|
|
// Workaround to call Activity.reportFullyDrawn from Jetpack Compose.
|
|
|
|
// This code should be called when the UI is ready for use
|
|
|
|
// This code should be called when the UI is ready for use
|
|
|
@ -142,8 +145,7 @@ internal fun ForYouScreen(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val state = rememberLazyGridState()
|
|
|
|
TrackScrollJank(scrollableState = lazyGridState, stateName = "forYou:feed")
|
|
|
|
TrackScrollJank(scrollableState = state, stateName = "forYou:feed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LazyVerticalGrid(
|
|
|
|
LazyVerticalGrid(
|
|
|
|
columns = Adaptive(300.dp),
|
|
|
|
columns = Adaptive(300.dp),
|
|
|
@ -153,15 +155,36 @@ internal fun ForYouScreen(
|
|
|
|
modifier = modifier
|
|
|
|
modifier = modifier
|
|
|
|
.fillMaxSize()
|
|
|
|
.fillMaxSize()
|
|
|
|
.testTag("forYou:feed"),
|
|
|
|
.testTag("forYou:feed"),
|
|
|
|
state = state
|
|
|
|
state = lazyGridState
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
onboarding(
|
|
|
|
items(
|
|
|
|
onboardingUiState = onboardingUiState,
|
|
|
|
items = forYouItems,
|
|
|
|
|
|
|
|
key = ForYouItem::key,
|
|
|
|
|
|
|
|
contentType = ForYouItem::contentType,
|
|
|
|
|
|
|
|
span = { item ->
|
|
|
|
|
|
|
|
when (item) {
|
|
|
|
|
|
|
|
is ForYouItem.OnBoarding -> GridItemSpan(maxLineSpan)
|
|
|
|
|
|
|
|
is ForYouItem.News -> GridItemSpan(1)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
itemContent = { item ->
|
|
|
|
|
|
|
|
when (item) {
|
|
|
|
|
|
|
|
is ForYouItem.News -> when (item) {
|
|
|
|
|
|
|
|
is ForYouItem.News.Loading -> Unit
|
|
|
|
|
|
|
|
is ForYouItem.News.Loaded -> NewsItem(
|
|
|
|
|
|
|
|
modifier = Modifier.animateItemPlacement(),
|
|
|
|
|
|
|
|
userNewsResource = item.userNewsResource,
|
|
|
|
|
|
|
|
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
is ForYouItem.OnBoarding -> OnboardingItem(
|
|
|
|
|
|
|
|
onboardingUiState = item.onboardingUiState,
|
|
|
|
onTopicCheckedChanged = onTopicCheckedChanged,
|
|
|
|
onTopicCheckedChanged = onTopicCheckedChanged,
|
|
|
|
saveFollowedTopics = saveFollowedTopics,
|
|
|
|
saveFollowedTopics = saveFollowedTopics,
|
|
|
|
// Custom LayoutModifier to remove the enforced parent 16.dp contentPadding
|
|
|
|
// Custom LayoutModifier to remove the enforced parent 16.dp contentPadding
|
|
|
|
// from the LazyVerticalGrid and enable edge-to-edge scrolling for this section
|
|
|
|
// from the LazyVerticalGrid and enable edge-to-edge scrolling for this section
|
|
|
|
interestsItemModifier = Modifier.layout { measurable, constraints ->
|
|
|
|
interestsItemModifier = Modifier
|
|
|
|
|
|
|
|
.layout { measurable, constraints ->
|
|
|
|
val placeable = measurable.measure(
|
|
|
|
val placeable = measurable.measure(
|
|
|
|
constraints.copy(
|
|
|
|
constraints.copy(
|
|
|
|
maxWidth = constraints.maxWidth + 32.dp.roundToPx()
|
|
|
|
maxWidth = constraints.maxWidth + 32.dp.roundToPx()
|
|
|
@ -171,22 +194,16 @@ internal fun ForYouScreen(
|
|
|
|
placeable.place(0, 0)
|
|
|
|
placeable.place(0, 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.animateItemPlacement()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
newsFeed(
|
|
|
|
|
|
|
|
feedState = feedState,
|
|
|
|
|
|
|
|
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
|
|
|
|
// Add space for the content to clear the "offline" snackbar.
|
|
|
|
|
|
|
|
// TODO: Check that the Scaffold handles this correctly in NiaApp
|
|
|
|
|
|
|
|
// if (isOffline) Spacer(modifier = Modifier.height(48.dp))
|
|
|
|
|
|
|
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
item {
|
|
|
|
|
|
|
|
SpacerItem(
|
|
|
|
|
|
|
|
modifier = Modifier.animateItemPlacement(),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
AnimatedVisibility(
|
|
|
|
AnimatedVisibility(
|
|
|
|
visible = isSyncing || isFeedLoading || isOnboardingLoading,
|
|
|
|
visible = isSyncing || isFeedLoading || isOnboardingLoading,
|
|
|
@ -216,7 +233,8 @@ internal fun ForYouScreen(
|
|
|
|
* Depending on the [onboardingUiState], this might emit no items.
|
|
|
|
* Depending on the [onboardingUiState], this might emit no items.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
private fun LazyGridScope.onboarding(
|
|
|
|
@Composable
|
|
|
|
|
|
|
|
fun OnboardingItem(
|
|
|
|
onboardingUiState: OnboardingUiState,
|
|
|
|
onboardingUiState: OnboardingUiState,
|
|
|
|
onTopicCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
onTopicCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
saveFollowedTopics: () -> Unit,
|
|
|
|
saveFollowedTopics: () -> Unit,
|
|
|
@ -228,7 +246,6 @@ private fun LazyGridScope.onboarding(
|
|
|
|
OnboardingUiState.NotShown -> Unit
|
|
|
|
OnboardingUiState.NotShown -> Unit
|
|
|
|
|
|
|
|
|
|
|
|
is OnboardingUiState.Shown -> {
|
|
|
|
is OnboardingUiState.Shown -> {
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
|
|
|
|
|
Column(modifier = interestsItemModifier) {
|
|
|
|
Column(modifier = interestsItemModifier) {
|
|
|
|
Text(
|
|
|
|
Text(
|
|
|
|
text = stringResource(R.string.onboarding_guidance_title),
|
|
|
|
text = stringResource(R.string.onboarding_guidance_title),
|
|
|
@ -272,6 +289,16 @@ private fun LazyGridScope.onboarding(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
|
|
private fun SpacerItem(modifier: Modifier = Modifier) {
|
|
|
|
|
|
|
|
Column(modifier = modifier) {
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
|
|
|
|
// Add space for the content to clear the "offline" snackbar.
|
|
|
|
|
|
|
|
// TODO: Check that the Scaffold handles this correctly in NiaApp
|
|
|
|
|
|
|
|
// if (isOffline) Spacer(modifier = Modifier.height(48.dp))
|
|
|
|
|
|
|
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
@Composable
|
|
|
@ -393,10 +420,7 @@ fun ForYouScreenPopulatedFeed() {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
isSyncing = false,
|
|
|
|
isSyncing = false,
|
|
|
|
onboardingUiState = OnboardingUiState.NotShown,
|
|
|
|
forYouItems = previewUserNewsResources.map(ForYouItem.News::Loaded),
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
|
|
|
|
feed = previewUserNewsResources
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
@ -412,10 +436,7 @@ fun ForYouScreenOfflinePopulatedFeed() {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
isSyncing = false,
|
|
|
|
isSyncing = false,
|
|
|
|
onboardingUiState = OnboardingUiState.NotShown,
|
|
|
|
forYouItems = emptyList(),
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
|
|
|
|
feed = previewUserNewsResources
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
@ -431,12 +452,7 @@ fun ForYouScreenTopicSelection() {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
isSyncing = false,
|
|
|
|
isSyncing = false,
|
|
|
|
onboardingUiState = OnboardingUiState.Shown(
|
|
|
|
forYouItems = emptyList(),
|
|
|
|
topics = previewTopics.map { FollowableTopic(it, false) },
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
|
|
|
|
feed = previewUserNewsResources
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
@ -452,8 +468,7 @@ fun ForYouScreenLoading() {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
isSyncing = false,
|
|
|
|
isSyncing = false,
|
|
|
|
onboardingUiState = OnboardingUiState.Loading,
|
|
|
|
forYouItems = emptyList(),
|
|
|
|
feedState = NewsFeedUiState.Loading,
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
@ -469,10 +484,7 @@ fun ForYouScreenPopulatedAndLoading() {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
isSyncing = true,
|
|
|
|
isSyncing = true,
|
|
|
|
onboardingUiState = OnboardingUiState.Loading,
|
|
|
|
forYouItems = emptyList(),
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
|
|
|
|
feed = previewUserNewsResources
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|