diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt index b9c72414d..bf5db6c48 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt @@ -5,7 +5,11 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.DrawerValue @@ -465,7 +469,6 @@ fun ArticleScreen( }, onFilterSelect = selectFilter, onSelectToday = { selectToday() }, - onSelectStatus = { selectStatus(it) }, refreshState = refreshAllState, onRefresh = { refreshAll() @@ -541,13 +544,24 @@ fun ArticleScreen( } }, bottomBar = { - audioEnclosure?.let { audio -> - FloatingAudioPlayer( - audio = audio, - controller = audioController, - onDismiss = { - audioController.dismiss() - }, + Column { + audioEnclosure?.let { audio -> + FloatingAudioPlayer( + audio = audio, + controller = audioController, + onDismiss = { + audioController.dismiss() + }, + // The status bar below is bottom-most, so it owns the + // navigation bar inset the player would otherwise pad for + modifier = Modifier + .consumeWindowInsets(WindowInsets.navigationBars), + ) + } + + ArticleStatusBottomBar( + status = filter.status, + onSelectStatus = { selectStatus(it) }, ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt index e2babcae1..1cb704179 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt @@ -1,56 +1,137 @@ package com.capyreader.app.ui.articles +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.material3.ButtonGroupDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ToggleButton +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.capyreader.app.ui.navigationTitle +import com.capyreader.app.ui.theme.CapyTheme import com.jocmp.capy.ArticleStatus -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ArticleStatusBar( onSelectStatus: (status: ArticleStatus) -> Unit, status: ArticleStatus, + modifier: Modifier = Modifier, ) { Row( - horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, ) { - options.forEachIndexed { index, buttonStatus -> - ToggleButton( - checked = buttonStatus == status, - onCheckedChange = { - if (buttonStatus != status) { - onSelectStatus(buttonStatus) - } - }, - modifier = Modifier.weight(1f), - shapes = when (index) { - 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() - options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() - else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + options.forEach { buttonStatus -> + StatusButton( + status = buttonStatus, + selected = buttonStatus == status, + onSelect = { onSelectStatus(buttonStatus) }, + ) + } + } +} + +@Composable +private fun StatusButton( + status: ArticleStatus, + selected: Boolean, + onSelect: () -> Unit, +) { + val label = stringResource(status.navigationTitle) + val colors = MaterialTheme.colorScheme + + val containerColor by animateColorAsState( + targetValue = when { + selected -> colors.surfaceContainerHighest + else -> Color.Transparent + }, + label = "statusContainer", + ) + val contentColor by animateColorAsState( + targetValue = when { + selected -> colors.onSurface + else -> colors.onSurfaceVariant + }, + label = "statusContent", + ) + + Surface( + onClick = { + if (!selected) { + onSelect() + } + }, + selected = selected, + shape = CircleShape, + color = containerColor, + contentColor = contentColor, + modifier = Modifier.semantics { contentDescription = label }, + ) { + Row( + modifier = Modifier + .defaultMinSize(minWidth = 48.dp, minHeight = 40.dp) + .padding(horizontal = 14.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + ArticleStatusIcon( + status = status, + modifier = Modifier.size(iconSize(status)), + ) + + AnimatedVisibility(visible = selected) { + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(Modifier.width(8.dp)) + Text( + text = label.uppercase(), + style = MaterialTheme.typography.labelMedium, + letterSpacing = 0.8.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } - ) { - ArticleStatusIcon(status = buttonStatus) } } } } +private fun iconSize(status: ArticleStatus) = when (status) { + ArticleStatus.UNREAD -> 12.dp + else -> 20.dp +} + val options = listOf( - ArticleStatus.ALL, - ArticleStatus.UNREAD, ArticleStatus.STARRED, + ArticleStatus.UNREAD, + ArticleStatus.ALL, ) -@Composable @Preview -fun ArticleStatusBarPreview() { - ArticleStatusBar( - onSelectStatus = {}, - status = ArticleStatus.UNREAD - ) +@Composable +private fun ArticleStatusBarPreview() { + CapyTheme { + ArticleStatusBar( + onSelectStatus = {}, + status = ArticleStatus.UNREAD, + ) + } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBottomBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBottomBar.kt new file mode 100644 index 000000000..6d0608f4e --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBottomBar.kt @@ -0,0 +1,53 @@ +package com.capyreader.app.ui.articles + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.capyreader.app.ui.theme.CapyTheme +import com.jocmp.capy.ArticleStatus + +@Composable +fun ArticleStatusBottomBar( + status: ArticleStatus, + onSelectStatus: (status: ArticleStatus) -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + ArticleStatusBar( + status = status, + onSelectStatus = onSelectStatus, + ) + } + } +} + +@Preview +@Composable +private fun ArticleStatusBottomBarPreview() { + CapyTheme { + ArticleStatusBottomBar( + status = ArticleStatus.UNREAD, + onSelectStatus = {}, + ) + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt index 1e1f22477..b75e5a118 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt @@ -6,24 +6,25 @@ import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import com.jocmp.capy.ArticleStatus @Composable -fun ArticleStatusIcon(status: ArticleStatus) { - return when (status) { - ArticleStatus.ALL -> Icon( - Icons.AutoMirrored.Rounded.Notes, - contentDescription = null - ) - - ArticleStatus.UNREAD -> Icon( - Icons.Rounded.FiberManualRecord, - contentDescription = null, - ) +fun ArticleStatusIcon( + status: ArticleStatus, + modifier: Modifier = Modifier, +) { + Icon( + imageVector = status.icon, + contentDescription = null, + modifier = modifier, + ) +} - ArticleStatus.STARRED -> Icon( - Icons.Rounded.Star, - contentDescription = null, - ) +val ArticleStatus.icon: ImageVector + get() = when (this) { + ArticleStatus.ALL -> Icons.AutoMirrored.Rounded.Notes + ArticleStatus.UNREAD -> Icons.Rounded.FiberManualRecord + ArticleStatus.STARRED -> Icons.Rounded.Star } -} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt index 32a296e49..9756ecfd5 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.unit.dp import com.capyreader.app.R import com.capyreader.app.common.FeedGroup import com.capyreader.app.ui.articles.AddFeedButton -import com.capyreader.app.ui.articles.ArticleStatusBar import com.capyreader.app.ui.articles.ArticleStatusIcon import com.capyreader.app.ui.articles.CountBadge import com.capyreader.app.ui.articles.ListTitle @@ -42,7 +41,6 @@ import com.capyreader.app.ui.navigationTitle import com.capyreader.app.ui.savedSearchNavTitle import com.capyreader.app.ui.theme.CapyTheme import com.jocmp.capy.ArticleFilter -import com.jocmp.capy.ArticleStatus import com.jocmp.capy.Feed import com.jocmp.capy.Folder import com.jocmp.capy.SavedSearch @@ -60,7 +58,6 @@ fun FeedList( savedSearches: List = emptyList(), onFilterSelect: () -> Unit, onSelectToday: () -> Unit, - onSelectStatus: (status: ArticleStatus) -> Unit, onSelectSavedSearch: (search: SavedSearch) -> Unit, refreshState: AngleRefreshState, onRefresh: () -> Unit, @@ -75,225 +72,206 @@ fun FeedList( val buttonState = rememberRefreshButtonState(refreshState) Column( - Modifier.fillMaxSize() + Modifier + .verticalScroll(scrollState) + .padding(horizontal = 12.dp) + .fillMaxSize() ) { - Column( - Modifier - .verticalScroll(scrollState) - .padding(horizontal = 12.dp) - .weight(1f) - .fillMaxSize() + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { + CapyIcon() Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { - CapyIcon() - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { onNavigateToSettings() }) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = stringResource(R.string.settings) - ) - } - IconButton(onClick = { onRefresh() }) { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = stringResource(R.string.feed_nav_drawer_refresh_all), - modifier = Modifier.graphicsLayer { - rotationZ = buttonState.iconRotation - } - ) - } - AddFeedButton( - iconOnly = true, - onComplete = { onFeedAdded(it) }, - onBeforeAdd = onBeforeFeedAdd, + IconButton(onClick = { onNavigateToSettings() }) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = stringResource(R.string.settings) + ) + } + IconButton(onClick = { onRefresh() }) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = stringResource(R.string.feed_nav_drawer_refresh_all), + modifier = Modifier.graphicsLayer { + rotationZ = buttonState.iconRotation + } ) } + AddFeedButton( + iconOnly = true, + onComplete = { onFeedAdded(it) }, + onBeforeAdd = onBeforeFeedAdd, + ) } + } - Box { - val (showArticlesMenu, setShowArticlesMenu) = remember { mutableStateOf(false) } + Box { + val (showArticlesMenu, setShowArticlesMenu) = remember { mutableStateOf(false) } - DrawerItem( - icon = { ArticleStatusIcon(status = filter.status) }, - label = { - ListTitle( - stringResource(filter.status.navigationTitle), - ) - }, - badge = { CountBadge(count = statusCount) }, - selected = filter.hasArticlesSelected(), - onClick = { onFilterSelect() }, - onLongClick = { setShowArticlesMenu(true) }, - ) + DrawerItem( + icon = { ArticleStatusIcon(status = filter.status) }, + label = { + ListTitle( + stringResource(filter.status.navigationTitle), + ) + }, + badge = { CountBadge(count = statusCount) }, + selected = filter.hasArticlesSelected(), + onClick = { onFilterSelect() }, + onLongClick = { setShowArticlesMenu(true) }, + ) - MarkAllReadMenu( - expanded = showArticlesMenu, - onDismiss = { setShowArticlesMenu(false) }, - onMarkAllRead = { - onMarkAllRead(ArticleFilter.Articles(articleStatus = filter.status)) - }, - ) - } + MarkAllReadMenu( + expanded = showArticlesMenu, + onDismiss = { setShowArticlesMenu(false) }, + onMarkAllRead = { + onMarkAllRead(ArticleFilter.Articles(articleStatus = filter.status)) + }, + ) + } + + Box { + val (showTodayMenu, setShowTodayMenu) = remember { mutableStateOf(false) } + + DrawerItem( + icon = { + Icon( + Icons.Rounded.Today, + contentDescription = null + ) + }, + label = { + ListTitle( + stringResource(R.string.filter_today), + ) + }, + badge = { CountBadge(count = todayCount) }, + selected = filter.hasTodaySelected(), + onClick = { onSelectToday() }, + onLongClick = { setShowTodayMenu(true) }, + ) + MarkAllReadMenu( + expanded = showTodayMenu, + onDismiss = { setShowTodayMenu(false) }, + onMarkAllRead = { + onMarkAllRead(ArticleFilter.Today(filter.status)) + }, + ) + } + + if (readLaterFeed != null) { Box { - val (showTodayMenu, setShowTodayMenu) = remember { mutableStateOf(false) } + val (showReadLaterMenu, setShowReadLaterMenu) = remember { mutableStateOf(false) } DrawerItem( icon = { Icon( - Icons.Rounded.Today, + Icons.Rounded.Bookmark, contentDescription = null ) }, label = { ListTitle( - stringResource(R.string.filter_today), + stringResource(R.string.filter_read_later), ) }, - badge = { CountBadge(count = todayCount) }, - selected = filter.hasTodaySelected(), - onClick = { onSelectToday() }, - onLongClick = { setShowTodayMenu(true) }, + badge = { CountBadge(count = readLaterFeed.count) }, + selected = filter.isFeedSelected(readLaterFeed), + onClick = { onSelectFeed(readLaterFeed, null) }, + onLongClick = { setShowReadLaterMenu(true) }, ) MarkAllReadMenu( - expanded = showTodayMenu, - onDismiss = { setShowTodayMenu(false) }, + expanded = showReadLaterMenu, + onDismiss = { setShowReadLaterMenu(false) }, onMarkAllRead = { - onMarkAllRead(ArticleFilter.Today(filter.status)) + onMarkAllRead( + ArticleFilter.Feeds( + feedID = readLaterFeed.id, + folderTitle = null, + feedStatus = filter.status, + ) + ) }, ) } + } - if (readLaterFeed != null) { - Box { - val (showReadLaterMenu, setShowReadLaterMenu) = remember { mutableStateOf(false) } + Spacer(Modifier.height(8.dp)) - DrawerItem( - icon = { - Icon( - Icons.Rounded.Bookmark, - contentDescription = null - ) - }, - label = { - ListTitle( - stringResource(R.string.filter_read_later), - ) + if (savedSearches.isNotEmpty()) { + FeedListDivider() + FeedGroupList( + type = FeedGroup.SAVED_SEARCHES, + title = stringResource(source.savedSearchNavTitle), + ) { + savedSearches.forEach { + SavedSearchRow( + onSelect = onSelectSavedSearch, + selected = filter.isSavedSearchSelected(it), + savedSearch = it, + ) + } + } + } + + if (folders.isNotEmpty()) { + FeedListDivider() + FeedGroupList( + type = FeedGroup.FOLDERS, + title = stringResource(R.string.nav_headline_folders) + ) { + folders.forEach { folder -> + FolderRow( + folder = folder, + onFolderSelect = onSelectFolder, + onFeedSelect = { feed -> + onSelectFeed(feed, folder.title) }, - badge = { CountBadge(count = readLaterFeed.count) }, - selected = filter.isFeedSelected(readLaterFeed), - onClick = { onSelectFeed(readLaterFeed, null) }, - onLongClick = { setShowReadLaterMenu(true) }, + onMarkAllRead = onMarkAllRead, + filter = filter, + source = source, ) + } + } + } - MarkAllReadMenu( - expanded = showReadLaterMenu, - onDismiss = { setShowReadLaterMenu(false) }, + if (feeds.isNotEmpty()) { + FeedListDivider() + FeedGroupList( + type = FeedGroup.FEEDS, + title = stringResource(R.string.nav_headline_feeds), + ) { + feeds.forEach { feed -> + FeedRow( + feed = feed, + onSelect = { + onSelectFeed(it, null) + }, onMarkAllRead = { onMarkAllRead( ArticleFilter.Feeds( - feedID = readLaterFeed.id, + feedID = feed.id, folderTitle = null, feedStatus = filter.status, ) ) }, + selected = filter.isFeedSelected(feed), + source = source, ) } } - - Spacer(Modifier.height(8.dp)) - - if (savedSearches.isNotEmpty()) { - FeedListDivider() - FeedGroupList( - type = FeedGroup.SAVED_SEARCHES, - title = stringResource(source.savedSearchNavTitle), - ) { - savedSearches.forEach { - SavedSearchRow( - onSelect = onSelectSavedSearch, - selected = filter.isSavedSearchSelected(it), - savedSearch = it, - ) - } - } - } - - if (folders.isNotEmpty()) { - FeedListDivider() - FeedGroupList( - type = FeedGroup.FOLDERS, - title = stringResource(R.string.nav_headline_folders) - ) { - folders.forEach { folder -> - FolderRow( - folder = folder, - onFolderSelect = onSelectFolder, - onFeedSelect = { feed -> - onSelectFeed(feed, folder.title) - }, - onMarkAllRead = onMarkAllRead, - filter = filter, - source = source, - ) - } - } - } - - if (feeds.isNotEmpty()) { - FeedListDivider() - FeedGroupList( - type = FeedGroup.FEEDS, - title = stringResource(R.string.nav_headline_feeds), - ) { - feeds.forEach { feed -> - FeedRow( - feed = feed, - onSelect = { - onSelectFeed(it, null) - }, - onMarkAllRead = { - onMarkAllRead( - ArticleFilter.Feeds( - feedID = feed.id, - folderTitle = null, - feedStatus = filter.status, - ) - ) - }, - selected = filter.isFeedSelected(feed), - source = source, - ) - } - } - } - - Spacer(Modifier.height(16.dp)) } - HorizontalDivider() - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.Center - ) { - ArticleStatusBar( - status = filter.status, - onSelectStatus = onSelectStatus, - ) - } + Spacer(Modifier.height(16.dp)) } } @@ -315,7 +293,6 @@ fun FeedListPreview() { onRefresh = {}, onFilterSelect = {}, onSelectToday = {}, - onSelectStatus = {}, filter = ArticleFilter.default(), statusCount = 10, todayCount = 5,