From 4dff4f842584703b9d7f4226e720cb156fdaa86e Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 9 Apr 2024 10:22:50 +0530 Subject: [PATCH 1/4] Fix feeds sort order in the bottom bar --- .../rss/reader/feeds/FeedsPresenter.kt | 2 +- .../rss/reader/feeds/ui/FeedsBottomSheet.kt | 68 +++++++++++-------- .../rss/reader/repository/FeedsOrderBy.kt | 3 +- .../dev/sasikanth/rss/reader/database/Feed.sq | 1 + 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt index 140572b24..4086108b6 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/FeedsPresenter.kt @@ -297,7 +297,7 @@ class FeedsPresenter( settingsRepository.postsType.distinctUntilChanged().flatMapLatest { postsType -> val postsAfter = postsAfterInstantFromPostsType(postsType) - feedsPager(postsAfter, FeedsOrderBy.Latest).cachedIn(coroutineScope) + feedsPager(postsAfter, FeedsOrderBy.Pinned).cachedIn(coroutineScope) } observableSelectedFeed.selectedFeed diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt index 30964b6c5..29e7db399 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt @@ -665,6 +665,11 @@ private fun AllFeedsHeader( FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical + FeedsOrderBy.Pinned -> { + throw IllegalStateException( + "Cannot use the following feed sort order here: $feedsSortOrder" + ) + } } Text( @@ -687,36 +692,43 @@ private fun AllFeedsHeader( expanded = showSortDropdown, onDismissRequest = { showSortDropdown = false } ) { - FeedsOrderBy.entries.forEach { sortOrder -> - val label = - when (sortOrder) { - FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest - FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest - FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical - } + FeedsOrderBy.entries + .filter { it != FeedsOrderBy.Pinned } + .forEach { sortOrder -> + val label = + when (sortOrder) { + FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest + FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest + FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical + FeedsOrderBy.Pinned -> { + throw IllegalStateException( + "Cannot use the following feed sort order here: $feedsSortOrder" + ) + } + } - val color = - if (feedsSortOrder == sortOrder) { - AppTheme.colorScheme.tintedSurface - } else { - Color.Unspecified - } - val labelColor = - if (feedsSortOrder == sortOrder) { - AppTheme.colorScheme.onSurface - } else { - AppTheme.colorScheme.textEmphasisHigh - } + val color = + if (feedsSortOrder == sortOrder) { + AppTheme.colorScheme.tintedSurface + } else { + Color.Unspecified + } + val labelColor = + if (feedsSortOrder == sortOrder) { + AppTheme.colorScheme.onSurface + } else { + AppTheme.colorScheme.textEmphasisHigh + } - DropdownMenuItem( - modifier = Modifier.background(color), - onClick = { - onFeedsSortChanged(sortOrder) - showSortDropdown = false - }, - text = { Text(label, color = labelColor) } - ) - } + DropdownMenuItem( + modifier = Modifier.background(color), + onClick = { + onFeedsSortChanged(sortOrder) + showSortDropdown = false + }, + text = { Text(label, color = labelColor) } + ) + } } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/FeedsOrderBy.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/FeedsOrderBy.kt index 3ce012710..f196e874b 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/FeedsOrderBy.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/FeedsOrderBy.kt @@ -19,5 +19,6 @@ package dev.sasikanth.rss.reader.repository enum class FeedsOrderBy(val value: String) { Latest("latest"), Oldest("oldest"), - Alphabetical("alphabetical") + Alphabetical("alphabetical"), + Pinned("pinned") } diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq index 928746e85..0accbde2e 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Feed.sq @@ -53,6 +53,7 @@ ORDER BY CASE WHEN :orderBy = 'latest' THEN f.createdAt END DESC, CASE WHEN :orderBy = 'oldest' THEN f.createdAt END ASC, CASE WHEN :orderBy = 'alphabetical' THEN f.name END ASC, + CASE WHEN :orderBy = 'pinned' THEN f.pinnedAt END DESC, f.createdAt DESC LIMIT :limit OFFSET :offset; From e745c6ace4b7a2da05072e07721eeee321bf5fb7 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 9 Apr 2024 10:28:12 +0530 Subject: [PATCH 2/4] Extract out common functions for calculating feed list item paddings --- .../rss/reader/feeds/ui/FeedsBottomSheet.kt | 117 ++++++------------ 1 file changed, 39 insertions(+), 78 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt index 29e7db399..0f95dbaaa 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt @@ -432,32 +432,10 @@ private fun LazyGridScope.feedSearchResults( span = { gridItemSpan } ) { index -> val feed = searchResults[index] - val startPadding = - when { - gridItemSpan.currentLineSpan == 2 || - (gridItemSpan.currentLineSpan == 1 && index % 2 == 0) -> 24.dp - else -> 8.dp - } - - val endPadding = - when { - gridItemSpan.currentLineSpan == 2 || - (gridItemSpan.currentLineSpan == 1 && index % 2 == 1) -> 24.dp - else -> 8.dp - } - - val topPadding = - when { - gridItemSpan.currentLineSpan == 2 && index > 0 -> 8.dp - gridItemSpan.currentLineSpan == 1 && index > 1 -> 8.dp - else -> 0.dp - } - - val bottomPadding = - when { - index < searchResults.itemCount -> 8.dp - else -> 0.dp - } + val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) + val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) + val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) + val bottomPadding = bottomPAddingOfFeedListItem(index, searchResults.itemCount) if (feed != null) { FeedListItem( @@ -502,32 +480,10 @@ private fun LazyGridScope.allFeeds( span = { gridItemSpan } ) { index -> val feed = feeds[index] - val startPadding = - when { - gridItemSpan.currentLineSpan == 2 || - (gridItemSpan.currentLineSpan == 1 && index % 2 == 0) -> 24.dp - else -> 8.dp - } - - val endPadding = - when { - gridItemSpan.currentLineSpan == 2 || - (gridItemSpan.currentLineSpan == 1 && index % 2 == 1) -> 24.dp - else -> 8.dp - } - - val topPadding = - when { - gridItemSpan.currentLineSpan == 2 && index > 0 -> 8.dp - gridItemSpan.currentLineSpan == 1 && index > 1 -> 8.dp - else -> 0.dp - } - - val bottomPadding = - when { - index < feeds.itemCount -> 8.dp - else -> 0.dp - } + val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) + val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) + val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) + val bottomPadding = bottomPAddingOfFeedListItem(index, feeds.itemCount) if (feed != null) { FeedListItem( @@ -573,32 +529,10 @@ private fun LazyGridScope.pinnedFeeds( span = { gridItemSpan } ) { index -> val feed = pinnedFeeds[index] - val startPadding = - when { - gridItemSpan.currentLineSpan == 2 || - (gridItemSpan.currentLineSpan == 1 && index % 2 == 0) -> 24.dp - else -> 8.dp - } - - val endPadding = - when { - gridItemSpan.currentLineSpan == 2 || - (gridItemSpan.currentLineSpan == 1 && index % 2 == 1) -> 24.dp - else -> 8.dp - } - - val topPadding = - when { - gridItemSpan.currentLineSpan == 2 && index > 0 -> 8.dp - gridItemSpan.currentLineSpan == 1 && index > 1 -> 8.dp - else -> 0.dp - } - - val bottomPadding = - when { - index < pinnedFeeds.itemCount -> 8.dp - else -> 0.dp - } + val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) + val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) + val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) + val bottomPadding = bottomPAddingOfFeedListItem(index, pinnedFeeds.itemCount) if (feed != null) { FeedListItem( @@ -627,6 +561,33 @@ private fun LazyGridScope.pinnedFeeds( } } +private fun bottomPAddingOfFeedListItem(index: Int, itemCount: Int) = + when { + index < itemCount -> 8.dp + else -> 0.dp + } + +private fun topPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = + when { + gridItemSpan.currentLineSpan == 2 && index > 0 -> 8.dp + gridItemSpan.currentLineSpan == 1 && index > 1 -> 8.dp + else -> 0.dp + } + +private fun endPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = + when { + gridItemSpan.currentLineSpan == 2 || (gridItemSpan.currentLineSpan == 1 && index % 2 == 1) -> + 24.dp + else -> 8.dp + } + +private fun startPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = + when { + gridItemSpan.currentLineSpan == 2 || (gridItemSpan.currentLineSpan == 1 && index % 2 == 0) -> + 24.dp + else -> 8.dp + } + @Composable private fun AllFeedsHeader( feedsCount: Int, From 38ff33e669b1385cdd7384a2c64ddb9dc89ffff1 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 9 Apr 2024 10:30:18 +0530 Subject: [PATCH 3/4] Extract out bottom sheet expanded & collapsed content into separate files --- .../feeds/ui/BottomSheetCollapsedContent.kt | 91 +++ .../feeds/ui/BottomSheetExpandedContent.kt | 591 ++++++++++++++++ .../rss/reader/feeds/ui/FeedsBottomSheet.kt | 634 ------------------ 3 files changed, 682 insertions(+), 634 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt new file mode 100644 index 000000000..94753d13f --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.feeds.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import app.cash.paging.compose.LazyPagingItems +import dev.sasikanth.rss.reader.core.model.local.Feed +import dev.sasikanth.rss.reader.ui.AppTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun BottomSheetCollapsedContent( + feeds: LazyPagingItems, + selectedFeed: Feed?, + canShowUnreadPostsCount: Boolean, + onFeedSelected: (Feed) -> Unit, + onHomeSelected: () -> Unit, + modifier: Modifier = Modifier +) { + LazyRow( + modifier = modifier.fillMaxWidth().padding(start = 20.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(end = 24.dp) + ) { + stickyHeader { + val shadowColors = + arrayOf( + 0.85f to AppTheme.colorScheme.tintedBackground, + 0.9f to AppTheme.colorScheme.tintedBackground.copy(alpha = 0.4f), + 1f to Color.Transparent + ) + + HomeBottomBarItem( + selected = selectedFeed == null, + onClick = onHomeSelected, + modifier = + Modifier.drawWithCache { + onDrawBehind { + val brush = + Brush.horizontalGradient( + colorStops = shadowColors, + ) + drawRect( + brush = brush, + ) + } + } + .padding(end = 4.dp) + ) + } + + items(feeds.itemCount) { index -> + val feed = feeds[index] + if (feed != null) { + FeedBottomBarItem( + text = feed.name.uppercase(), + badgeCount = feed.numberOfUnreadPosts, + iconUrl = feed.icon, + canShowUnreadPostsCount = canShowUnreadPostsCount, + selected = selectedFeed?.link == feed.link, + onClick = { onFeedSelected(feed) } + ) + } + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt new file mode 100644 index 000000000..2f9839878 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt @@ -0,0 +1,591 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.feeds.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.outlined.ViewAgenda +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import app.cash.paging.compose.LazyPagingItems +import app.cash.paging.compose.itemContentType +import app.cash.paging.compose.itemKey +import dev.sasikanth.rss.reader.components.DropdownMenu +import dev.sasikanth.rss.reader.components.DropdownMenuItem +import dev.sasikanth.rss.reader.core.model.local.Feed +import dev.sasikanth.rss.reader.repository.FeedsOrderBy +import dev.sasikanth.rss.reader.resources.strings.LocalStrings +import dev.sasikanth.rss.reader.ui.AppTheme +import dev.sasikanth.rss.reader.utils.Constants +import dev.sasikanth.rss.reader.utils.KeyboardState +import dev.sasikanth.rss.reader.utils.keyboardVisibilityAsState + +@Composable +internal fun BottomSheetExpandedContent( + feeds: LazyPagingItems, + pinnedFeeds: LazyPagingItems, + searchResults: LazyPagingItems, + searchQuery: TextFieldValue, + feedsSortOrder: FeedsOrderBy, + feedsViewMode: FeedsViewMode, + isPinnedSectionExpanded: Boolean, + canShowUnreadPostsCount: Boolean, + onSearchQueryChanged: (TextFieldValue) -> Unit, + onClearSearchQuery: () -> Unit, + onFeedInfoClick: (Feed) -> Unit, + onFeedSelected: (Feed) -> Unit, + onTogglePinnedSection: () -> Unit, + onFeedsSortChanged: (FeedsOrderBy) -> Unit, + onChangeFeedsViewModeClick: () -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + modifier = Modifier.fillMaxSize().consumeWindowInsets(WindowInsets.statusBars).then(modifier), + topBar = { + SearchBar( + query = searchQuery, + feedsViewMode = feedsViewMode, + onQueryChange = { onSearchQueryChanged(it) }, + onClearClick = onClearSearchQuery, + onChangeFeedsViewModeClick = onChangeFeedsViewModeClick + ) + }, + bottomBar = { + BottomBar( + onNewGroupClick = { + // TODO: Open group creation dialog/sheet/screen + }, + onNewFeedClick = { + // TODO: Open feed creation dialog/sheet/screen + } + ) + }, + containerColor = AppTheme.colorScheme.tintedBackground + ) { padding -> + val layoutDirection = LocalLayoutDirection.current + val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() + + val gridItemSpan = + when (feedsViewMode) { + FeedsViewMode.Grid -> GridItemSpan(1) + FeedsViewMode.List -> GridItemSpan(2) + } + + LazyVerticalGrid( + modifier = + Modifier.fillMaxSize() + .padding( + bottom = if (imeBottomPadding > 0.dp) imeBottomPadding + 16.dp else 0.dp, + // doing this so that the dividers in sticky headers can go below the search bar and + // not overlap with each other + top = padding.calculateTopPadding() - 1.dp + ), + columns = GridCells.Fixed(2), + contentPadding = + PaddingValues( + start = padding.calculateStartPadding(layoutDirection), + end = padding.calculateEndPadding(layoutDirection), + bottom = padding.calculateBottomPadding() + 64.dp, + top = 8.dp + ), + ) { + if ( + searchResults.itemCount == 0 && + searchQuery.text.length < Constants.MINIMUM_REQUIRED_SEARCH_CHARACTERS + ) { + pinnedFeeds( + pinnedFeeds = pinnedFeeds, + isPinnedSectionExpanded = isPinnedSectionExpanded, + canShowUnreadPostsCount = canShowUnreadPostsCount, + gridItemSpan = gridItemSpan, + onTogglePinnedSection = onTogglePinnedSection, + onFeedInfoClick = onFeedInfoClick, + onFeedSelected = onFeedSelected + ) + + allFeeds( + feeds = feeds, + feedsSortOrder = feedsSortOrder, + canShowUnreadPostsCount = canShowUnreadPostsCount, + gridItemSpan = gridItemSpan, + onFeedsSortChanged = onFeedsSortChanged, + onFeedInfoClick = onFeedInfoClick, + onFeedSelected = onFeedSelected + ) + } else { + feedSearchResults( + searchResults = searchResults, + canShowUnreadPostsCount = canShowUnreadPostsCount, + gridItemSpan = gridItemSpan, + onFeedInfoClick = onFeedInfoClick, + onFeedSelected = onFeedSelected + ) + } + } + } +} + +@Composable +private fun SearchBar( + query: TextFieldValue, + feedsViewMode: FeedsViewMode, + onQueryChange: (TextFieldValue) -> Unit, + onClearClick: () -> Unit, + onChangeFeedsViewModeClick: () -> Unit, +) { + val keyboardState by keyboardVisibilityAsState() + val focusManager = LocalFocusManager.current + + LaunchedEffect(keyboardState) { + if (keyboardState == KeyboardState.Closed) { + focusManager.clearFocus() + } + } + + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + MaterialTheme(colorScheme = darkColorScheme(primary = AppTheme.colorScheme.tintedForeground)) { + OutlinedTextField( + modifier = + Modifier.weight(1f) + .windowInsetsPadding( + WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) + .padding(vertical = 8.dp) + .padding(start = 24.dp, end = 12.dp), + value = query.copy(selection = TextRange(query.text.length)), + onValueChange = onQueryChange, + placeholder = { + Text( + text = LocalStrings.current.feedsSearchHint, + color = AppTheme.colorScheme.tintedForeground, + style = MaterialTheme.typography.bodyLarge + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null, + tint = AppTheme.colorScheme.tintedForeground + ) + }, + trailingIcon = { + if (query.text.isNotBlank()) { + IconButton(onClick = onClearClick) { + Icon( + Icons.Rounded.Close, + contentDescription = null, + tint = AppTheme.colorScheme.tintedForeground + ) + } + } + }, + shape = RoundedCornerShape(16.dp), + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = AppTheme.colorScheme.tintedHighlight, + unfocusedBorderColor = AppTheme.colorScheme.tintedHighlight, + disabledBorderColor = AppTheme.colorScheme.tintedHighlight, + focusedTextColor = AppTheme.colorScheme.textEmphasisHigh, + disabledTextColor = Color.Transparent, + ) + ) + } + + IconButton( + onClick = onChangeFeedsViewModeClick, + ) { + val icon = + when (feedsViewMode) { + FeedsViewMode.Grid -> Icons.Outlined.ViewAgenda + FeedsViewMode.List -> Icons.Filled.GridView + } + + Icon( + imageVector = icon, + contentDescription = null, + tint = AppTheme.colorScheme.tintedForeground + ) + } + + Spacer(Modifier.requiredWidth(20.dp)) + } +} + +private fun LazyGridScope.feedSearchResults( + searchResults: LazyPagingItems, + canShowUnreadPostsCount: Boolean, + gridItemSpan: GridItemSpan, + onFeedInfoClick: (Feed) -> Unit, + onFeedSelected: (Feed) -> Unit +) { + items( + count = searchResults.itemCount, + key = searchResults.itemKey { it.link }, + contentType = searchResults.itemContentType { it.link }, + span = { gridItemSpan } + ) { index -> + val feed = searchResults[index] + val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) + val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) + val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) + val bottomPadding = bottomPAddingOfFeedListItem(index, searchResults.itemCount) + + if (feed != null) { + FeedListItem( + modifier = + Modifier.padding( + start = startPadding, + top = topPadding, + end = endPadding, + bottom = bottomPadding + ), + feed = feed, + canShowUnreadPostsCount = canShowUnreadPostsCount, + onFeedInfoClick = onFeedInfoClick, + onFeedSelected = onFeedSelected, + ) + } + } +} + +private fun LazyGridScope.allFeeds( + feeds: LazyPagingItems, + feedsSortOrder: FeedsOrderBy, + canShowUnreadPostsCount: Boolean, + gridItemSpan: GridItemSpan, + onFeedsSortChanged: (FeedsOrderBy) -> Unit, + onFeedInfoClick: (Feed) -> Unit, + onFeedSelected: (Feed) -> Unit +) { + if (feeds.itemCount > 0 && feeds.loadState.refresh != LoadState.Loading) { + item(key = "AllFeedsHeader", span = { GridItemSpan(2) }) { + AllFeedsHeader( + feedsCount = feeds.itemCount, + feedsSortOrder = feedsSortOrder, + onFeedsSortChanged = onFeedsSortChanged + ) + } + + items( + count = feeds.itemCount, + key = feeds.itemKey { it.link }, + contentType = { "FeedListItem" }, + span = { gridItemSpan } + ) { index -> + val feed = feeds[index] + val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) + val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) + val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) + val bottomPadding = bottomPAddingOfFeedListItem(index, feeds.itemCount) + + if (feed != null) { + FeedListItem( + modifier = + Modifier.padding( + start = startPadding, + top = topPadding, + end = endPadding, + bottom = bottomPadding + ), + feed = feed, + canShowUnreadPostsCount = canShowUnreadPostsCount, + onFeedInfoClick = onFeedInfoClick, + onFeedSelected = onFeedSelected, + ) + } + } + } +} + +private fun LazyGridScope.pinnedFeeds( + pinnedFeeds: LazyPagingItems, + isPinnedSectionExpanded: Boolean, + canShowUnreadPostsCount: Boolean, + gridItemSpan: GridItemSpan, + onTogglePinnedSection: () -> Unit, + onFeedInfoClick: (Feed) -> Unit, + onFeedSelected: (Feed) -> Unit +) { + if (pinnedFeeds.itemCount > 0 && pinnedFeeds.loadState.refresh != LoadState.Loading) { + item(key = "PinnedFeedsHeader", span = { GridItemSpan(2) }) { + PinnedFeedsHeader( + isPinnedSectionExpanded = isPinnedSectionExpanded, + onToggleSection = onTogglePinnedSection + ) + } + + if (isPinnedSectionExpanded) { + items( + count = pinnedFeeds.itemCount, + key = pinnedFeeds.itemKey { "PinnedFeed:${it.link}" }, + contentType = { "FeedListItem" }, + span = { gridItemSpan } + ) { index -> + val feed = pinnedFeeds[index] + val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) + val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) + val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) + val bottomPadding = bottomPAddingOfFeedListItem(index, pinnedFeeds.itemCount) + + if (feed != null) { + FeedListItem( + modifier = + Modifier.padding( + start = startPadding, + top = topPadding, + end = endPadding, + bottom = bottomPadding + ), + feed = feed, + canShowUnreadPostsCount = canShowUnreadPostsCount, + onFeedInfoClick = onFeedInfoClick, + onFeedSelected = onFeedSelected, + ) + } + } + } + + item(span = { GridItemSpan(2) }) { + HorizontalDivider( + modifier = Modifier.padding(top = 24.dp), + color = AppTheme.colorScheme.tintedSurface + ) + } + } +} + +private fun bottomPAddingOfFeedListItem(index: Int, itemCount: Int) = + when { + index < itemCount -> 8.dp + else -> 0.dp + } + +private fun topPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = + when { + gridItemSpan.currentLineSpan == 2 && index > 0 -> 8.dp + gridItemSpan.currentLineSpan == 1 && index > 1 -> 8.dp + else -> 0.dp + } + +private fun endPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = + when { + gridItemSpan.currentLineSpan == 2 || (gridItemSpan.currentLineSpan == 1 && index % 2 == 1) -> + 24.dp + else -> 8.dp + } + +private fun startPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = + when { + gridItemSpan.currentLineSpan == 2 || (gridItemSpan.currentLineSpan == 1 && index % 2 == 0) -> + 24.dp + else -> 8.dp + } + +@Composable +private fun AllFeedsHeader( + feedsCount: Int, + feedsSortOrder: FeedsOrderBy, + onFeedsSortChanged: (FeedsOrderBy) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = + Modifier.padding(start = 32.dp, end = 20.dp).padding(vertical = 12.dp).then(modifier), + verticalAlignment = Alignment.CenterVertically + ) { + var showSortDropdown by remember { mutableStateOf(false) } + + Text( + text = LocalStrings.current.allFeeds, + style = MaterialTheme.typography.titleMedium, + color = AppTheme.colorScheme.textEmphasisHigh, + ) + + Spacer(Modifier.requiredWidth(8.dp)) + + Text( + modifier = Modifier.weight(1f), + text = feedsCount.toString(), + style = MaterialTheme.typography.titleMedium, + color = AppTheme.colorScheme.tintedForeground, + ) + + Spacer(Modifier.requiredWidth(12.dp)) + + Box { + TextButton(onClick = { showSortDropdown = true }, shape = MaterialTheme.shapes.large) { + val orderText = + when (feedsSortOrder) { + FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest + FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest + FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical + FeedsOrderBy.Pinned -> { + throw IllegalStateException( + "Cannot use the following feed sort order here: $feedsSortOrder" + ) + } + } + + Text( + text = orderText, + style = MaterialTheme.typography.labelLarge, + color = AppTheme.colorScheme.tintedForeground + ) + + Spacer(Modifier.width(8.dp)) + + Icon( + imageVector = Icons.Filled.ExpandMore, + contentDescription = LocalStrings.current.editFeeds, + tint = AppTheme.colorScheme.tintedForeground + ) + } + + DropdownMenu( + modifier = Modifier.requiredWidth(132.dp), + expanded = showSortDropdown, + onDismissRequest = { showSortDropdown = false } + ) { + FeedsOrderBy.entries + .filter { it != FeedsOrderBy.Pinned } + .forEach { sortOrder -> + val label = + when (sortOrder) { + FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest + FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest + FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical + FeedsOrderBy.Pinned -> { + throw IllegalStateException( + "Cannot use the following feed sort order here: $feedsSortOrder" + ) + } + } + + val color = + if (feedsSortOrder == sortOrder) { + AppTheme.colorScheme.tintedSurface + } else { + Color.Unspecified + } + val labelColor = + if (feedsSortOrder == sortOrder) { + AppTheme.colorScheme.onSurface + } else { + AppTheme.colorScheme.textEmphasisHigh + } + + DropdownMenuItem( + modifier = Modifier.background(color), + onClick = { + onFeedsSortChanged(sortOrder) + showSortDropdown = false + }, + text = { Text(label, color = labelColor) } + ) + } + } + } + } +} + +@Composable +private fun PinnedFeedsHeader( + isPinnedSectionExpanded: Boolean, + modifier: Modifier = Modifier, + onToggleSection: () -> Unit +) { + Row( + modifier = + Modifier.padding(start = 32.dp, end = 20.dp).padding(vertical = 12.dp).then(modifier), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = LocalStrings.current.pinnedFeeds, + style = MaterialTheme.typography.titleMedium, + color = AppTheme.colorScheme.textEmphasisHigh, + ) + + val icon = + if (isPinnedSectionExpanded) { + Icons.Filled.ExpandLess + } else { + Icons.Filled.ExpandMore + } + + Spacer(Modifier.requiredWidth(12.dp)) + + IconButton(onClick = onToggleSection) { + Icon(imageVector = icon, contentDescription = null, tint = AppTheme.colorScheme.onSurface) + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt index 0f95dbaaa..9281b220f 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedsBottomSheet.kt @@ -16,89 +16,20 @@ package dev.sasikanth.rss.reader.feeds.ui import androidx.compose.animation.core.Transition -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyGridScope -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.GridView -import androidx.compose.material.icons.outlined.ViewAgenda -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.paging.LoadState -import app.cash.paging.compose.LazyPagingItems import app.cash.paging.compose.collectAsLazyPagingItems -import app.cash.paging.compose.itemContentType -import app.cash.paging.compose.itemKey -import dev.sasikanth.rss.reader.components.DropdownMenu -import dev.sasikanth.rss.reader.components.DropdownMenuItem -import dev.sasikanth.rss.reader.core.model.local.Feed import dev.sasikanth.rss.reader.feeds.FeedsEffect import dev.sasikanth.rss.reader.feeds.FeedsEvent import dev.sasikanth.rss.reader.feeds.FeedsPresenter -import dev.sasikanth.rss.reader.repository.FeedsOrderBy -import dev.sasikanth.rss.reader.resources.strings.LocalStrings -import dev.sasikanth.rss.reader.ui.AppTheme -import dev.sasikanth.rss.reader.utils.Constants.MINIMUM_REQUIRED_SEARCH_CHARACTERS -import dev.sasikanth.rss.reader.utils.KeyboardState import dev.sasikanth.rss.reader.utils.inverse -import dev.sasikanth.rss.reader.utils.keyboardVisibilityAsState @Composable internal fun FeedsBottomSheet( @@ -173,568 +104,3 @@ internal fun FeedsBottomSheet( } } } - -@Composable -private fun BottomSheetExpandedContent( - feeds: LazyPagingItems, - pinnedFeeds: LazyPagingItems, - searchResults: LazyPagingItems, - searchQuery: TextFieldValue, - feedsSortOrder: FeedsOrderBy, - feedsViewMode: FeedsViewMode, - isPinnedSectionExpanded: Boolean, - canShowUnreadPostsCount: Boolean, - onSearchQueryChanged: (TextFieldValue) -> Unit, - onClearSearchQuery: () -> Unit, - onFeedInfoClick: (Feed) -> Unit, - onFeedSelected: (Feed) -> Unit, - onTogglePinnedSection: () -> Unit, - onFeedsSortChanged: (FeedsOrderBy) -> Unit, - onChangeFeedsViewModeClick: () -> Unit, - modifier: Modifier = Modifier -) { - Scaffold( - modifier = Modifier.fillMaxSize().consumeWindowInsets(WindowInsets.statusBars).then(modifier), - topBar = { - SearchBar( - query = searchQuery, - feedsViewMode = feedsViewMode, - onQueryChange = { onSearchQueryChanged(it) }, - onClearClick = onClearSearchQuery, - onChangeFeedsViewModeClick = onChangeFeedsViewModeClick - ) - }, - bottomBar = { - BottomBar( - onNewGroupClick = { - // TODO: Open group creation dialog/sheet/screen - }, - onNewFeedClick = { - // TODO: Open feed creation dialog/sheet/screen - } - ) - }, - containerColor = AppTheme.colorScheme.tintedBackground - ) { padding -> - val layoutDirection = LocalLayoutDirection.current - val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() - - val gridItemSpan = - when (feedsViewMode) { - FeedsViewMode.Grid -> GridItemSpan(1) - FeedsViewMode.List -> GridItemSpan(2) - } - - LazyVerticalGrid( - modifier = - Modifier.fillMaxSize() - .padding( - bottom = if (imeBottomPadding > 0.dp) imeBottomPadding + 16.dp else 0.dp, - // doing this so that the dividers in sticky headers can go below the search bar and - // not overlap with each other - top = padding.calculateTopPadding() - 1.dp - ), - columns = GridCells.Fixed(2), - contentPadding = - PaddingValues( - start = padding.calculateStartPadding(layoutDirection), - end = padding.calculateEndPadding(layoutDirection), - bottom = padding.calculateBottomPadding() + 64.dp, - top = 8.dp - ), - ) { - if ( - searchResults.itemCount == 0 && searchQuery.text.length < MINIMUM_REQUIRED_SEARCH_CHARACTERS - ) { - pinnedFeeds( - pinnedFeeds = pinnedFeeds, - isPinnedSectionExpanded = isPinnedSectionExpanded, - canShowUnreadPostsCount = canShowUnreadPostsCount, - gridItemSpan = gridItemSpan, - onTogglePinnedSection = onTogglePinnedSection, - onFeedInfoClick = onFeedInfoClick, - onFeedSelected = onFeedSelected - ) - - allFeeds( - feeds = feeds, - feedsSortOrder = feedsSortOrder, - canShowUnreadPostsCount = canShowUnreadPostsCount, - gridItemSpan = gridItemSpan, - onFeedsSortChanged = onFeedsSortChanged, - onFeedInfoClick = onFeedInfoClick, - onFeedSelected = onFeedSelected - ) - } else { - feedSearchResults( - searchResults = searchResults, - canShowUnreadPostsCount = canShowUnreadPostsCount, - gridItemSpan = gridItemSpan, - onFeedInfoClick = onFeedInfoClick, - onFeedSelected = onFeedSelected - ) - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun BottomSheetCollapsedContent( - feeds: LazyPagingItems, - selectedFeed: Feed?, - canShowUnreadPostsCount: Boolean, - onFeedSelected: (Feed) -> Unit, - onHomeSelected: () -> Unit, - modifier: Modifier = Modifier -) { - LazyRow( - modifier = modifier.fillMaxWidth().padding(start = 20.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(end = 24.dp) - ) { - stickyHeader { - val shadowColors = - arrayOf( - 0.85f to AppTheme.colorScheme.tintedBackground, - 0.9f to AppTheme.colorScheme.tintedBackground.copy(alpha = 0.4f), - 1f to Color.Transparent - ) - - HomeBottomBarItem( - selected = selectedFeed == null, - onClick = onHomeSelected, - modifier = - Modifier.drawWithCache { - onDrawBehind { - val brush = - Brush.horizontalGradient( - colorStops = shadowColors, - ) - drawRect( - brush = brush, - ) - } - } - .padding(end = 4.dp) - ) - } - - items(feeds.itemCount) { index -> - val feed = feeds[index] - if (feed != null) { - FeedBottomBarItem( - text = feed.name.uppercase(), - badgeCount = feed.numberOfUnreadPosts, - iconUrl = feed.icon, - canShowUnreadPostsCount = canShowUnreadPostsCount, - selected = selectedFeed?.link == feed.link, - onClick = { onFeedSelected(feed) } - ) - } - } - } -} - -@Composable -private fun SearchBar( - query: TextFieldValue, - feedsViewMode: FeedsViewMode, - onQueryChange: (TextFieldValue) -> Unit, - onClearClick: () -> Unit, - onChangeFeedsViewModeClick: () -> Unit, -) { - val keyboardState by keyboardVisibilityAsState() - val focusManager = LocalFocusManager.current - - LaunchedEffect(keyboardState) { - if (keyboardState == KeyboardState.Closed) { - focusManager.clearFocus() - } - } - - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - MaterialTheme(colorScheme = darkColorScheme(primary = AppTheme.colorScheme.tintedForeground)) { - OutlinedTextField( - modifier = - Modifier.weight(1f) - .windowInsetsPadding( - WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) - .padding(vertical = 8.dp) - .padding(start = 24.dp, end = 12.dp), - value = query.copy(selection = TextRange(query.text.length)), - onValueChange = onQueryChange, - placeholder = { - Text( - text = LocalStrings.current.feedsSearchHint, - color = AppTheme.colorScheme.tintedForeground, - style = MaterialTheme.typography.bodyLarge - ) - }, - leadingIcon = { - Icon( - imageVector = Icons.Rounded.Search, - contentDescription = null, - tint = AppTheme.colorScheme.tintedForeground - ) - }, - trailingIcon = { - if (query.text.isNotBlank()) { - ClearSearchQueryButton { onClearClick() } - } - }, - shape = RoundedCornerShape(16.dp), - singleLine = true, - textStyle = MaterialTheme.typography.bodyLarge, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = AppTheme.colorScheme.tintedHighlight, - unfocusedBorderColor = AppTheme.colorScheme.tintedHighlight, - disabledBorderColor = AppTheme.colorScheme.tintedHighlight, - focusedTextColor = AppTheme.colorScheme.textEmphasisHigh, - disabledTextColor = Color.Transparent, - ) - ) - } - - IconButton( - onClick = onChangeFeedsViewModeClick, - ) { - val icon = - when (feedsViewMode) { - FeedsViewMode.Grid -> Icons.Outlined.ViewAgenda - FeedsViewMode.List -> Icons.Filled.GridView - } - - Icon( - imageVector = icon, - contentDescription = null, - tint = AppTheme.colorScheme.tintedForeground - ) - } - - Spacer(Modifier.requiredWidth(20.dp)) - } -} - -private fun LazyGridScope.feedSearchResults( - searchResults: LazyPagingItems, - canShowUnreadPostsCount: Boolean, - gridItemSpan: GridItemSpan, - onFeedInfoClick: (Feed) -> Unit, - onFeedSelected: (Feed) -> Unit -) { - items( - count = searchResults.itemCount, - key = searchResults.itemKey { it.link }, - contentType = searchResults.itemContentType { it.link }, - span = { gridItemSpan } - ) { index -> - val feed = searchResults[index] - val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) - val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) - val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) - val bottomPadding = bottomPAddingOfFeedListItem(index, searchResults.itemCount) - - if (feed != null) { - FeedListItem( - modifier = - Modifier.padding( - start = startPadding, - top = topPadding, - end = endPadding, - bottom = bottomPadding - ), - feed = feed, - canShowUnreadPostsCount = canShowUnreadPostsCount, - onFeedInfoClick = onFeedInfoClick, - onFeedSelected = onFeedSelected, - ) - } - } -} - -private fun LazyGridScope.allFeeds( - feeds: LazyPagingItems, - feedsSortOrder: FeedsOrderBy, - canShowUnreadPostsCount: Boolean, - gridItemSpan: GridItemSpan, - onFeedsSortChanged: (FeedsOrderBy) -> Unit, - onFeedInfoClick: (Feed) -> Unit, - onFeedSelected: (Feed) -> Unit -) { - if (feeds.itemCount > 0 && feeds.loadState.refresh != LoadState.Loading) { - item(key = "AllFeedsHeader", span = { GridItemSpan(2) }) { - AllFeedsHeader( - feedsCount = feeds.itemCount, - feedsSortOrder = feedsSortOrder, - onFeedsSortChanged = onFeedsSortChanged - ) - } - - items( - count = feeds.itemCount, - key = feeds.itemKey { it.link }, - contentType = { "FeedListItem" }, - span = { gridItemSpan } - ) { index -> - val feed = feeds[index] - val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) - val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) - val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) - val bottomPadding = bottomPAddingOfFeedListItem(index, feeds.itemCount) - - if (feed != null) { - FeedListItem( - modifier = - Modifier.padding( - start = startPadding, - top = topPadding, - end = endPadding, - bottom = bottomPadding - ), - feed = feed, - canShowUnreadPostsCount = canShowUnreadPostsCount, - onFeedInfoClick = onFeedInfoClick, - onFeedSelected = onFeedSelected, - ) - } - } - } -} - -private fun LazyGridScope.pinnedFeeds( - pinnedFeeds: LazyPagingItems, - isPinnedSectionExpanded: Boolean, - canShowUnreadPostsCount: Boolean, - gridItemSpan: GridItemSpan, - onTogglePinnedSection: () -> Unit, - onFeedInfoClick: (Feed) -> Unit, - onFeedSelected: (Feed) -> Unit -) { - if (pinnedFeeds.itemCount > 0 && pinnedFeeds.loadState.refresh != LoadState.Loading) { - item(key = "PinnedFeedsHeader", span = { GridItemSpan(2) }) { - PinnedFeedsHeader( - isPinnedSectionExpanded = isPinnedSectionExpanded, - onToggleSection = onTogglePinnedSection - ) - } - - if (isPinnedSectionExpanded) { - items( - count = pinnedFeeds.itemCount, - key = pinnedFeeds.itemKey { "PinnedFeed:${it.link}" }, - contentType = { "FeedListItem" }, - span = { gridItemSpan } - ) { index -> - val feed = pinnedFeeds[index] - val startPadding = startPaddingOfFeedListItem(gridItemSpan, index) - val endPadding = endPaddingOfFeedListItem(gridItemSpan, index) - val topPadding = topPaddingOfFeedListItem(gridItemSpan, index) - val bottomPadding = bottomPAddingOfFeedListItem(index, pinnedFeeds.itemCount) - - if (feed != null) { - FeedListItem( - modifier = - Modifier.padding( - start = startPadding, - top = topPadding, - end = endPadding, - bottom = bottomPadding - ), - feed = feed, - canShowUnreadPostsCount = canShowUnreadPostsCount, - onFeedInfoClick = onFeedInfoClick, - onFeedSelected = onFeedSelected, - ) - } - } - } - - item(span = { GridItemSpan(2) }) { - HorizontalDivider( - modifier = Modifier.padding(top = 24.dp), - color = AppTheme.colorScheme.tintedSurface - ) - } - } -} - -private fun bottomPAddingOfFeedListItem(index: Int, itemCount: Int) = - when { - index < itemCount -> 8.dp - else -> 0.dp - } - -private fun topPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = - when { - gridItemSpan.currentLineSpan == 2 && index > 0 -> 8.dp - gridItemSpan.currentLineSpan == 1 && index > 1 -> 8.dp - else -> 0.dp - } - -private fun endPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = - when { - gridItemSpan.currentLineSpan == 2 || (gridItemSpan.currentLineSpan == 1 && index % 2 == 1) -> - 24.dp - else -> 8.dp - } - -private fun startPaddingOfFeedListItem(gridItemSpan: GridItemSpan, index: Int) = - when { - gridItemSpan.currentLineSpan == 2 || (gridItemSpan.currentLineSpan == 1 && index % 2 == 0) -> - 24.dp - else -> 8.dp - } - -@Composable -private fun AllFeedsHeader( - feedsCount: Int, - feedsSortOrder: FeedsOrderBy, - onFeedsSortChanged: (FeedsOrderBy) -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = - Modifier.padding(start = 32.dp, end = 20.dp).padding(vertical = 12.dp).then(modifier), - verticalAlignment = Alignment.CenterVertically - ) { - var showSortDropdown by remember { mutableStateOf(false) } - - Text( - text = LocalStrings.current.allFeeds, - style = MaterialTheme.typography.titleMedium, - color = AppTheme.colorScheme.textEmphasisHigh, - ) - - Spacer(Modifier.requiredWidth(8.dp)) - - Text( - modifier = Modifier.weight(1f), - text = feedsCount.toString(), - style = MaterialTheme.typography.titleMedium, - color = AppTheme.colorScheme.tintedForeground, - ) - - Spacer(Modifier.requiredWidth(12.dp)) - - Box { - TextButton(onClick = { showSortDropdown = true }, shape = MaterialTheme.shapes.large) { - val orderText = - when (feedsSortOrder) { - FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest - FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest - FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical - FeedsOrderBy.Pinned -> { - throw IllegalStateException( - "Cannot use the following feed sort order here: $feedsSortOrder" - ) - } - } - - Text( - text = orderText, - style = MaterialTheme.typography.labelLarge, - color = AppTheme.colorScheme.tintedForeground - ) - - Spacer(Modifier.width(8.dp)) - - Icon( - imageVector = Icons.Filled.ExpandMore, - contentDescription = LocalStrings.current.editFeeds, - tint = AppTheme.colorScheme.tintedForeground - ) - } - - DropdownMenu( - modifier = Modifier.requiredWidth(132.dp), - expanded = showSortDropdown, - onDismissRequest = { showSortDropdown = false } - ) { - FeedsOrderBy.entries - .filter { it != FeedsOrderBy.Pinned } - .forEach { sortOrder -> - val label = - when (sortOrder) { - FeedsOrderBy.Latest -> LocalStrings.current.feedsSortLatest - FeedsOrderBy.Oldest -> LocalStrings.current.feedsSortOldest - FeedsOrderBy.Alphabetical -> LocalStrings.current.feedsSortAlphabetical - FeedsOrderBy.Pinned -> { - throw IllegalStateException( - "Cannot use the following feed sort order here: $feedsSortOrder" - ) - } - } - - val color = - if (feedsSortOrder == sortOrder) { - AppTheme.colorScheme.tintedSurface - } else { - Color.Unspecified - } - val labelColor = - if (feedsSortOrder == sortOrder) { - AppTheme.colorScheme.onSurface - } else { - AppTheme.colorScheme.textEmphasisHigh - } - - DropdownMenuItem( - modifier = Modifier.background(color), - onClick = { - onFeedsSortChanged(sortOrder) - showSortDropdown = false - }, - text = { Text(label, color = labelColor) } - ) - } - } - } - } -} - -@Composable -private fun PinnedFeedsHeader( - isPinnedSectionExpanded: Boolean, - modifier: Modifier = Modifier, - onToggleSection: () -> Unit -) { - Row( - modifier = - Modifier.padding(start = 32.dp, end = 20.dp).padding(vertical = 12.dp).then(modifier), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.weight(1f), - text = LocalStrings.current.pinnedFeeds, - style = MaterialTheme.typography.titleMedium, - color = AppTheme.colorScheme.textEmphasisHigh, - ) - - val icon = - if (isPinnedSectionExpanded) { - Icons.Filled.ExpandLess - } else { - Icons.Filled.ExpandMore - } - - Spacer(Modifier.requiredWidth(12.dp)) - - IconButton(onClick = onToggleSection) { - Icon(imageVector = icon, contentDescription = null, tint = AppTheme.colorScheme.onSurface) - } - } -} - -@Composable -private fun ClearSearchQueryButton(onClearClick: () -> Unit) { - IconButton(onClick = onClearClick) { - Icon( - Icons.Rounded.Close, - contentDescription = null, - tint = AppTheme.colorScheme.tintedForeground - ) - } -} From 7a07d29f5703d9a92826abf0bf9d408d21246641 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 9 Apr 2024 10:39:17 +0530 Subject: [PATCH 4/4] Rename `BottomBar` to `BottomSheetExpandedBottomBar` --- .../feeds/ui/{BottomBar.kt => BottomSheetExpandedBottomBar.kt} | 2 +- .../sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/{BottomBar.kt => BottomSheetExpandedBottomBar.kt} (99%) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomBar.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedBottomBar.kt similarity index 99% rename from shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomBar.kt rename to shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedBottomBar.kt index 6dc1d21d7..113c6926f 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomBar.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedBottomBar.kt @@ -50,7 +50,7 @@ import dev.sasikanth.rss.reader.resources.strings.LocalStrings import dev.sasikanth.rss.reader.ui.AppTheme @Composable -fun BottomBar( +internal fun BottomSheetExpandedBottomBar( modifier: Modifier = Modifier, onNewGroupClick: () -> Unit, onNewFeedClick: () -> Unit, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt index 2f9839878..a3a86cd89 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt @@ -118,7 +118,7 @@ internal fun BottomSheetExpandedContent( ) }, bottomBar = { - BottomBar( + BottomSheetExpandedBottomBar( onNewGroupClick = { // TODO: Open group creation dialog/sheet/screen },