|
|
@ -17,7 +17,9 @@
|
|
|
|
package com.google.samples.apps.nowinandroid.feature.foryou
|
|
|
|
package com.google.samples.apps.nowinandroid.feature.foryou
|
|
|
|
|
|
|
|
|
|
|
|
import android.app.Activity
|
|
|
|
import android.app.Activity
|
|
|
|
|
|
|
|
import androidx.compose.animation.AnimatedVisibility
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
|
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
|
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
|
@ -35,7 +37,6 @@ import androidx.compose.foundation.layout.safeDrawing
|
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
|
|
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
|
|
|
import androidx.compose.foundation.layout.wrapContentSize
|
|
|
|
|
|
|
|
import androidx.compose.foundation.lazy.LazyListScope
|
|
|
|
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
|
|
|
@ -51,12 +52,16 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|
|
|
import androidx.compose.material3.Icon
|
|
|
|
import androidx.compose.material3.Icon
|
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
|
import androidx.compose.material3.Scaffold
|
|
|
|
import androidx.compose.material3.Scaffold
|
|
|
|
|
|
|
|
import androidx.compose.material3.SnackbarDuration.Indefinite
|
|
|
|
|
|
|
|
import androidx.compose.material3.SnackbarHost
|
|
|
|
|
|
|
|
import androidx.compose.material3.SnackbarHostState
|
|
|
|
import androidx.compose.material3.Surface
|
|
|
|
import androidx.compose.material3.Surface
|
|
|
|
import androidx.compose.material3.Text
|
|
|
|
import androidx.compose.material3.Text
|
|
|
|
import androidx.compose.material3.TopAppBarDefaults
|
|
|
|
import androidx.compose.material3.TopAppBarDefaults
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
|
|
|
import androidx.compose.runtime.remember
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
@ -77,7 +82,7 @@ import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
|
|
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
|
import coil.compose.AsyncImage
|
|
|
|
import coil.compose.AsyncImage
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
|
@ -101,7 +106,12 @@ fun ForYouRoute(
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
val interestsSelectionState by viewModel.interestsSelectionState.collectAsStateWithLifecycle()
|
|
|
|
val interestsSelectionState by viewModel.interestsSelectionState.collectAsStateWithLifecycle()
|
|
|
|
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
|
|
|
|
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
val isOffline by viewModel.isOffline.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
|
|
|
|
|
|
|
|
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
|
|
|
|
isOffline = isOffline,
|
|
|
|
|
|
|
|
isSyncing = isSyncing,
|
|
|
|
interestsSelectionState = interestsSelectionState,
|
|
|
|
interestsSelectionState = interestsSelectionState,
|
|
|
|
feedState = feedState,
|
|
|
|
feedState = feedState,
|
|
|
|
onTopicCheckedChanged = viewModel::updateTopicSelection,
|
|
|
|
onTopicCheckedChanged = viewModel::updateTopicSelection,
|
|
|
@ -115,6 +125,8 @@ fun ForYouRoute(
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
|
|
|
@Composable
|
|
|
|
@Composable
|
|
|
|
fun ForYouScreen(
|
|
|
|
fun ForYouScreen(
|
|
|
|
|
|
|
|
isOffline: Boolean,
|
|
|
|
|
|
|
|
isSyncing: Boolean,
|
|
|
|
interestsSelectionState: ForYouInterestsSelectionUiState,
|
|
|
|
interestsSelectionState: ForYouInterestsSelectionUiState,
|
|
|
|
feedState: NewsFeedUiState,
|
|
|
|
feedState: NewsFeedUiState,
|
|
|
|
onTopicCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
onTopicCheckedChanged: (String, Boolean) -> Unit,
|
|
|
@ -123,7 +135,10 @@ fun ForYouScreen(
|
|
|
|
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
modifier: Modifier = Modifier,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
|
|
|
|
val snackbarHostState = remember { SnackbarHostState() }
|
|
|
|
|
|
|
|
|
|
|
|
Scaffold(
|
|
|
|
Scaffold(
|
|
|
|
|
|
|
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
|
|
topBar = {
|
|
|
|
topBar = {
|
|
|
|
NiaTopAppBar(
|
|
|
|
NiaTopAppBar(
|
|
|
|
titleRes = R.string.top_app_bar_title,
|
|
|
|
titleRes = R.string.top_app_bar_title,
|
|
|
@ -165,6 +180,14 @@ fun ForYouScreen(
|
|
|
|
val lazyGridState = rememberLazyGridState()
|
|
|
|
val lazyGridState = rememberLazyGridState()
|
|
|
|
TrackScrollJank(scrollableState = lazyGridState, stateName = tag)
|
|
|
|
TrackScrollJank(scrollableState = lazyGridState, stateName = tag)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val notConnected = stringResource(R.string.for_you_not_connected)
|
|
|
|
|
|
|
|
LaunchedEffect(isOffline) {
|
|
|
|
|
|
|
|
if (isOffline) snackbarHostState.showSnackbar(
|
|
|
|
|
|
|
|
message = notConnected,
|
|
|
|
|
|
|
|
duration = Indefinite
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LazyVerticalGrid(
|
|
|
|
LazyVerticalGrid(
|
|
|
|
columns = Adaptive(300.dp),
|
|
|
|
columns = Adaptive(300.dp),
|
|
|
|
contentPadding = PaddingValues(16.dp),
|
|
|
|
contentPadding = PaddingValues(16.dp),
|
|
|
@ -198,21 +221,36 @@ fun ForYouScreen(
|
|
|
|
|
|
|
|
|
|
|
|
newsFeed(
|
|
|
|
newsFeed(
|
|
|
|
feedState = feedState,
|
|
|
|
feedState = feedState,
|
|
|
|
// Avoid showing a second loading wheel if we already are for the interests
|
|
|
|
|
|
|
|
// selection
|
|
|
|
|
|
|
|
showLoadingUIIfLoading =
|
|
|
|
|
|
|
|
interestsSelectionState !is ForYouInterestsSelectionUiState.Loading,
|
|
|
|
|
|
|
|
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
|
|
|
|
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
|
|
|
|
loadingContentDescription = R.string.for_you_loading
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
|
Column {
|
|
|
|
Column {
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
|
|
|
|
// Add space for the content to clear the "offline" snackbar.
|
|
|
|
|
|
|
|
if (isOffline) Spacer(modifier = Modifier.height(48.dp))
|
|
|
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
|
|
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AnimatedVisibility(
|
|
|
|
|
|
|
|
visible = isSyncing ||
|
|
|
|
|
|
|
|
feedState is NewsFeedUiState.Loading ||
|
|
|
|
|
|
|
|
interestsSelectionState is ForYouInterestsSelectionUiState.Loading
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
val loadingContentDescription = stringResource(id = R.string.for_you_loading)
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
|
|
.padding(innerPadding)
|
|
|
|
|
|
|
|
.consumedWindowInsets(innerPadding)
|
|
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
NiaOverlayLoadingWheel(
|
|
|
|
|
|
|
|
modifier = Modifier.align(Alignment.Center),
|
|
|
|
|
|
|
|
contentDesc = loadingContentDescription
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -220,9 +258,6 @@ fun ForYouScreen(
|
|
|
|
* An extension on [LazyListScope] defining the interests selection portion of the for you screen.
|
|
|
|
* An extension on [LazyListScope] defining the interests selection portion of the for you screen.
|
|
|
|
* Depending on the [interestsSelectionState], this might emit no items.
|
|
|
|
* Depending on the [interestsSelectionState], this might emit no items.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param showLoaderWhenLoading if true, show a visual indication of loading if the
|
|
|
|
|
|
|
|
* [interestsSelectionState] is loading. This is controllable to permit du-duplicating loading
|
|
|
|
|
|
|
|
* states.
|
|
|
|
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
private fun LazyGridScope.interestsSelection(
|
|
|
|
private fun LazyGridScope.interestsSelection(
|
|
|
|
interestsSelectionState: ForYouInterestsSelectionUiState,
|
|
|
|
interestsSelectionState: ForYouInterestsSelectionUiState,
|
|
|
@ -232,17 +267,8 @@ private fun LazyGridScope.interestsSelection(
|
|
|
|
interestsItemModifier: Modifier = Modifier
|
|
|
|
interestsItemModifier: Modifier = Modifier
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
when (interestsSelectionState) {
|
|
|
|
when (interestsSelectionState) {
|
|
|
|
ForYouInterestsSelectionUiState.Loading -> {
|
|
|
|
ForYouInterestsSelectionUiState.Loading,
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
|
ForYouInterestsSelectionUiState.LoadFailed,
|
|
|
|
NiaLoadingWheel(
|
|
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
|
|
.wrapContentSize()
|
|
|
|
|
|
|
|
.testTag("forYou:loading"),
|
|
|
|
|
|
|
|
contentDesc = stringResource(id = R.string.for_you_loading),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
ForYouInterestsSelectionUiState.NoInterestsSelection -> Unit
|
|
|
|
ForYouInterestsSelectionUiState.NoInterestsSelection -> Unit
|
|
|
|
is ForYouInterestsSelectionUiState.WithInterestsSelection -> {
|
|
|
|
is ForYouInterestsSelectionUiState.WithInterestsSelection -> {
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
|
|
@ -415,6 +441,31 @@ fun ForYouScreenPopulatedFeed() {
|
|
|
|
BoxWithConstraints {
|
|
|
|
BoxWithConstraints {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
|
|
|
|
isOffline = false,
|
|
|
|
|
|
|
|
isSyncing = false,
|
|
|
|
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
|
|
|
|
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
|
|
|
|
feed = previewNewsResources.map {
|
|
|
|
|
|
|
|
SaveableNewsResource(it, false)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
|
|
|
|
onAuthorCheckedChanged = { _, _ -> },
|
|
|
|
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@DevicePreviews
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
|
|
fun ForYouScreenOfflinePopulatedFeed() {
|
|
|
|
|
|
|
|
BoxWithConstraints {
|
|
|
|
|
|
|
|
NiaTheme {
|
|
|
|
|
|
|
|
ForYouScreen(
|
|
|
|
|
|
|
|
isOffline = true,
|
|
|
|
|
|
|
|
isSyncing = false,
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.NoInterestsSelection,
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
feed = previewNewsResources.map {
|
|
|
|
feed = previewNewsResources.map {
|
|
|
@ -436,6 +487,8 @@ fun ForYouScreenTopicSelection() {
|
|
|
|
BoxWithConstraints {
|
|
|
|
BoxWithConstraints {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
|
|
|
|
isOffline = false,
|
|
|
|
|
|
|
|
isSyncing = false,
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.WithInterestsSelection(
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.WithInterestsSelection(
|
|
|
|
topics = previewTopics.map { FollowableTopic(it, false) },
|
|
|
|
topics = previewTopics.map { FollowableTopic(it, false) },
|
|
|
|
authors = previewAuthors.map { FollowableAuthor(it, false) }
|
|
|
|
authors = previewAuthors.map { FollowableAuthor(it, false) }
|
|
|
@ -460,6 +513,8 @@ fun ForYouScreenLoading() {
|
|
|
|
BoxWithConstraints {
|
|
|
|
BoxWithConstraints {
|
|
|
|
NiaTheme {
|
|
|
|
NiaTheme {
|
|
|
|
ForYouScreen(
|
|
|
|
ForYouScreen(
|
|
|
|
|
|
|
|
isOffline = false,
|
|
|
|
|
|
|
|
isSyncing = false,
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.Loading,
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.Loading,
|
|
|
|
feedState = NewsFeedUiState.Loading,
|
|
|
|
feedState = NewsFeedUiState.Loading,
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
@ -470,3 +525,26 @@ fun ForYouScreenLoading() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@DevicePreviews
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
|
|
fun ForYouScreenPopulatedAndLoading() {
|
|
|
|
|
|
|
|
BoxWithConstraints {
|
|
|
|
|
|
|
|
NiaTheme {
|
|
|
|
|
|
|
|
ForYouScreen(
|
|
|
|
|
|
|
|
isOffline = false,
|
|
|
|
|
|
|
|
isSyncing = true,
|
|
|
|
|
|
|
|
interestsSelectionState = ForYouInterestsSelectionUiState.Loading,
|
|
|
|
|
|
|
|
feedState = NewsFeedUiState.Success(
|
|
|
|
|
|
|
|
feed = previewNewsResources.map {
|
|
|
|
|
|
|
|
SaveableNewsResource(it, false)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
onTopicCheckedChanged = { _, _ -> },
|
|
|
|
|
|
|
|
onAuthorCheckedChanged = { _, _ -> },
|
|
|
|
|
|
|
|
saveFollowedTopics = {},
|
|
|
|
|
|
|
|
onNewsResourcesCheckedChanged = { _, _ -> }
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|