feat: add selection mode UI to BookmarksScreen with checkboxes, counter, and cancel

Co-Authored-By: Claude <noreply@anthropic.com>
pull/2125/head
Rohit Karadkar 3 months ago
parent 6727e89415
commit 3bc15f9e25

@ -24,6 +24,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.activity.compose)
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.feature.bookmarks.api) implementation(projects.feature.bookmarks.api)
implementation(projects.feature.topic.api) implementation(projects.feature.topic.api)

@ -17,9 +17,11 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks.impl package com.google.samples.apps.nowinandroid.feature.bookmarks.impl
import android.net.Uri import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -42,8 +44,15 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
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
@ -74,6 +83,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadi
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme
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.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
@ -110,6 +120,12 @@ internal fun BookmarksScreen(
undoBookmarkRemoval = viewModel::undoBookmarkRemoval, undoBookmarkRemoval = viewModel::undoBookmarkRemoval,
clearUndoState = viewModel::clearUndoState, clearUndoState = viewModel::clearUndoState,
onEditNote = { editingNoteId = it }, onEditNote = { editingNoteId = it },
isInSelectionMode = viewModel.isInSelectionMode,
selectedIds = viewModel.selectedIds,
enterSelectionMode = viewModel::enterSelectionMode,
exitSelectionMode = viewModel::exitSelectionMode,
toggleSelection = viewModel::toggleSelection,
selectAll = viewModel::selectAll,
) )
editingNoteId?.let { id -> editingNoteId?.let { id ->
@ -127,6 +143,7 @@ internal fun BookmarksScreen(
/** /**
* Displays the user's bookmarked articles. Includes support for loading and empty states. * Displays the user's bookmarked articles. Includes support for loading and empty states.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@Composable @Composable
internal fun BookmarksScreen( internal fun BookmarksScreen(
@ -140,10 +157,20 @@ internal fun BookmarksScreen(
undoBookmarkRemoval: () -> Unit = {}, undoBookmarkRemoval: () -> Unit = {},
clearUndoState: () -> Unit = {}, clearUndoState: () -> Unit = {},
onEditNote: (String) -> Unit = {}, onEditNote: (String) -> Unit = {},
isInSelectionMode: Boolean = false,
selectedIds: Set<String> = emptySet(),
enterSelectionMode: (String) -> Unit = {},
exitSelectionMode: () -> Unit = {},
toggleSelection: (String) -> Unit = {},
selectAll: () -> Unit = {},
) { ) {
val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed) val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed)
val undoText = stringResource(id = R.string.feature_bookmarks_api_undo) val undoText = stringResource(id = R.string.feature_bookmarks_api_undo)
BackHandler(enabled = isInSelectionMode) {
exitSelectionMode()
}
LaunchedEffect(shouldDisplayUndoBookmark) { LaunchedEffect(shouldDisplayUndoBookmark) {
if (shouldDisplayUndoBookmark) { if (shouldDisplayUndoBookmark) {
val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText) val snackBarResult = onShowSnackbar(bookmarkRemovedMessage, undoText)
@ -159,19 +186,49 @@ internal fun BookmarksScreen(
clearUndoState() clearUndoState()
} }
when (feedState) { if (isInSelectionMode) {
Loading -> LoadingState(modifier) TopAppBar(
is Success -> if (feedState.feed.isNotEmpty()) { title = { Text("${selectedIds.size} selected") },
BookmarksGrid( navigationIcon = {
feedState, IconButton(onClick = exitSelectionMode) {
removeFromBookmarks, Icon(NiaIcons.Close, contentDescription = "Cancel selection")
onNewsResourceViewed, }
onTopicClick, },
onEditNote, actions = {
modifier, TextButton(onClick = selectAll) { Text("Select all") }
) },
} else { )
EmptyState(modifier) }
Box(modifier = modifier.fillMaxSize()) {
when (feedState) {
Loading -> LoadingState()
is Success -> if (feedState.feed.isNotEmpty()) {
BookmarksGrid(
feedState = feedState,
removeFromBookmarks = removeFromBookmarks,
onNewsResourceViewed = onNewsResourceViewed,
onTopicClick = onTopicClick,
onEditNote = onEditNote,
isInSelectionMode = isInSelectionMode,
selectedIds = selectedIds,
enterSelectionMode = enterSelectionMode,
toggleSelection = toggleSelection,
)
} else {
EmptyState()
}
}
if (isInSelectionMode && selectedIds.isNotEmpty()) {
Button(
onClick = { selectedIds.forEach { removeFromBookmarks(it) } },
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp),
) {
Text("Remove (${selectedIds.size})")
}
} }
} }
@ -196,6 +253,10 @@ private fun BookmarksGrid(
onNewsResourceViewed: (String) -> Unit, onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onEditNote: (String) -> Unit, onEditNote: (String) -> Unit,
isInSelectionMode: Boolean,
selectedIds: Set<String>,
enterSelectionMode: (String) -> Unit,
toggleSelection: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrollableState = rememberLazyStaggeredGridState() val scrollableState = rememberLazyStaggeredGridState()
@ -215,35 +276,50 @@ private fun BookmarksGrid(
.testTag("bookmarks:feed"), .testTag("bookmarks:feed"),
) { ) {
items( items(
items = feedState.feed, items = feedState.feed,
key = { it.id }, key = { it.id },
contentType = { "newsFeedItem" }, contentType = { "newsFeedItem" },
) { userNewsResource -> ) { userNewsResource ->
val context = LocalContext.current val context = LocalContext.current
val analyticsHelper = LocalAnalyticsHelper.current val analyticsHelper = LocalAnalyticsHelper.current
val backgroundColor = val backgroundColor =
MaterialTheme.colorScheme.background.toArgb() MaterialTheme.colorScheme.background.toArgb()
Column( Box(
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.animateItem(), .animateItem()
) { .combinedClickable(
onClick = {
if (isInSelectionMode) {
toggleSelection(userNewsResource.id)
}
},
onLongClick = {
if (!isInSelectionMode) {
enterSelectionMode(userNewsResource.id)
}
},
),
) {
Column {
NewsResourceCardExpanded( NewsResourceCardExpanded(
userNewsResource = userNewsResource, userNewsResource = userNewsResource,
isBookmarked = userNewsResource.isSaved, isBookmarked = userNewsResource.isSaved,
onClick = { onClick = {
analyticsHelper.logNewsResourceOpened( if (!isInSelectionMode) {
newsResourceId = userNewsResource.id, analyticsHelper.logNewsResourceOpened(
) newsResourceId = userNewsResource.id,
launchCustomChromeTab( )
context, launchCustomChromeTab(
Uri.parse(userNewsResource.url), context,
backgroundColor, Uri.parse(userNewsResource.url),
) backgroundColor,
onNewsResourceViewed(userNewsResource.id) )
onNewsResourceViewed(userNewsResource.id)
}
}, },
hasBeenViewed = userNewsResource.hasBeenViewed, hasBeenViewed = userNewsResource.hasBeenViewed,
onToggleBookmark = { removeFromBookmarks(userNewsResource.id) }, onToggleBookmark = { if (!isInSelectionMode) removeFromBookmarks(userNewsResource.id) },
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
) )
val note = userNewsResource.bookmarkNote val note = userNewsResource.bookmarkNote
@ -256,11 +332,27 @@ private fun BookmarksGrid(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onEditNote(userNewsResource.id) }, .then(
if (!isInSelectionMode) {
Modifier.clickable { onEditNote(userNewsResource.id) }
} else {
Modifier
},
),
) )
} }
} }
if (isInSelectionMode) {
Checkbox(
checked = userNewsResource.id in selectedIds,
onCheckedChange = { toggleSelection(userNewsResource.id) },
modifier = Modifier
.align(Alignment.TopStart)
.padding(4.dp),
)
}
} }
}
item(span = StaggeredGridItemSpan.FullLine) { item(span = StaggeredGridItemSpan.FullLine) {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
} }
@ -344,6 +436,10 @@ private fun BookmarksGridPreview(
onNewsResourceViewed = {}, onNewsResourceViewed = {},
onTopicClick = {}, onTopicClick = {},
onEditNote = {}, onEditNote = {},
isInSelectionMode = false,
selectedIds = emptySet(),
enterSelectionMode = {},
toggleSelection = {},
) )
} }
} }

Loading…
Cancel
Save