From 8efadd05995940252b0e8fc46df9b8db3a1575f0 Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Mon, 9 Dec 2024 23:58:02 -0600 Subject: [PATCH] Open drawer at end of feed --- .../app/ui/articles/ArticleLayout.kt | 32 ++++--- .../app/ui/articles/ArticleScreen.kt | 21 ++++- .../app/ui/articles/ArticleScreenViewModel.kt | 46 +++++++--- .../com/jocmp/capy/articles/NextFilter.kt | 86 +++++++++++++------ .../com/jocmp/capy/articles/NextFilterTest.kt | 20 ++--- 5 files changed, 143 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt index d7406565..8b00f53d 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt @@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.DrawerValue +import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration @@ -20,7 +20,6 @@ import androidx.compose.material3.TopAppBarState import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -91,7 +90,7 @@ fun ArticleLayout( onMarkAllRead: (range: MarkRead) -> Unit, onRequestNextFeed: () -> Unit, onRemoveFeed: (feedID: String, onSuccess: () -> Unit, onFailure: () -> Unit) -> Unit, - drawerValue: DrawerValue = DrawerValue.Closed, + drawerState: DrawerState, showUnauthorizedMessage: Boolean, onUnauthorizedDismissRequest: () -> Unit, canSwipeToNextFeed: Boolean, @@ -104,7 +103,6 @@ fun ArticleLayout( val (isUpdatePasswordDialogOpen, setUpdatePasswordDialogOpen) = rememberSaveable { mutableStateOf(false) } - val drawerState = rememberDrawerState(drawerValue) val coroutineScope = rememberCoroutineScope() val scaffoldNavigator = rememberArticleScaffoldNavigator() val hasMultipleColumns = scaffoldNavigator.scaffoldDirective.maxHorizontalPartitions > 1 @@ -145,17 +143,22 @@ fun ArticleLayout( scrollBehavior = scrollBehavior ) + suspend fun resetListVisibility() { + listState.scrollToItem(0) + resetScrollBehaviorOffset() + listVisible = true + } + suspend fun openNextStatus(action: suspend () -> Unit) { listVisible = false delay(200) action() scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.List) - resetScrollBehaviorOffset() coroutineScope.launch { delay(500) if (!listVisible) { - listVisible = true + resetListVisibility() } } } @@ -170,10 +173,14 @@ fun ArticleLayout( fun markAllRead(range: MarkRead) { if (range is MarkRead.All && filter !is ArticleFilter.Articles && canSwipeToNextFeed) { - requestNextFeed() + coroutineScope.launchUI { + openNextStatus { + onMarkAllRead(range) + } + } + } else { + onMarkAllRead(range) } - - onMarkAllRead(range) } val scrollToTop = { @@ -376,7 +383,9 @@ fun ArticleLayout( articles = pagingArticles, selectedArticleKey = article?.id, listState = listState, - onMarkAllRead = onMarkAllRead, + onMarkAllRead = { range -> + onMarkAllRead(range) + }, onSelect = { selectArticle(it) }, ) } @@ -481,8 +490,7 @@ fun ArticleLayout( LaunchedEffect(pagingArticles.itemCount) { if (!listVisible) { - listState.scrollToItem(0) - listVisible = true + resetListVisibility() } } } 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 d9e6bc6b..ae900817 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 @@ -1,16 +1,20 @@ package com.capyreader.app.ui.articles +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems import com.capyreader.app.common.AppPreferences import com.capyreader.app.ui.LocalConnectivity import com.capyreader.app.ui.components.ArticleSearch import com.capyreader.app.ui.rememberLocalConnectivity +import com.jocmp.capy.common.launchUI import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -30,10 +34,12 @@ fun ArticleScreen( val articles = viewModel.articles.collectAsLazyPagingItems() val nextFilter by viewModel.nextFilter.collectAsStateWithLifecycle(initialValue = null) val canSwipeToNextFeed = nextFilter != null + val scope = rememberCoroutineScope() val fullContent = rememberFullContent(viewModel) val articleActions = rememberArticleActions(viewModel) val connectivity = rememberLocalConnectivity() + val drawerState = rememberDrawerState(DrawerValue.Closed) CompositionLocalProvider( LocalFullContent provides fullContent, @@ -53,6 +59,7 @@ fun ArticleScreen( onFeedRefresh = { completion -> viewModel.refreshFeed(completion) }, + drawerState = drawerState, onSelectFolder = viewModel::selectFolder, onSelectFeed = viewModel::selectFeed, onSelectArticleFilter = viewModel::selectArticleFilter, @@ -62,7 +69,19 @@ fun ArticleScreen( onRequestClearArticle = viewModel::clearArticle, onToggleArticleRead = viewModel::toggleArticleRead, onToggleArticleStar = viewModel::toggleArticleStar, - onMarkAllRead = viewModel::markAllRead, + onMarkAllRead = { range -> + viewModel.markAllRead( + onEndOfList = { + scope.launchUI { + drawerState.open() + } + }, + range = range, + filter = filter, + feeds = feeds, + folders = folders, + ) + }, onRemoveFeed = viewModel::removeFeed, showUnauthorizedMessage = viewModel.showUnauthorizedMessage, onUnauthorizedDismissRequest = viewModel::dismissUnauthorizedMessage, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt index c141e1a5..b428dfcd 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt @@ -96,7 +96,7 @@ class ArticleScreenViewModel( private val nextFilterListener: Flow = combine(feeds, folders, filter) { feeds, folders, filter -> - NextFilter.find(filter, feeds, folders) + NextFilter.findSwipeDestination(filter, feeds, folders) } private val _nextFilter = MutableStateFlow(null) @@ -163,7 +163,13 @@ class ArticleScreenViewModel( } } - fun markAllRead(range: MarkRead) { + fun markAllRead( + onEndOfList: () -> Unit, + range: MarkRead, + filter: ArticleFilter, + feeds: List, + folders: List, + ) { viewModelScope.launchIO { val articleIDs = account.unreadArticleIDs( filter = latestFilter, @@ -174,6 +180,20 @@ class ArticleScreenViewModel( account.markAllRead(articleIDs).onFailure { Sync.markReadAsync(articleIDs, context) } + + if (range is MarkRead.All) { + val nextFilter = NextFilter.findMarkReadDestination(filter, folders, feeds) + + if (nextFilter != null) { + selectNextFilter(nextFilter) + } else { + if (filter.status == ArticleStatus.UNREAD) { + selectArticleFilter() + updateArticlesSince(OffsetDateTime.now().plusSeconds(1)) + } + onEndOfList() + } + } } } @@ -296,15 +316,17 @@ class ArticleScreenViewModel( } fun requestNextFeed() { - _nextFilter.value?.let { - when (it) { - is NextFilter.FeedFilter -> selectFeed( - feedID = it.feedID, - folderTitle = it.folderTitle - ) + _nextFilter.value?.let(::selectNextFilter) + } - is NextFilter.FolderFilter -> selectFolder(title = it.folderTitle) - } + private fun selectNextFilter(filter: NextFilter) { + when (filter) { + is NextFilter.FeedFilter -> selectFeed( + feedID = filter.feedID, + folderTitle = filter.folderTitle + ) + + is NextFilter.FolderFilter -> selectFolder(title = filter.folderTitle) } } @@ -366,8 +388,8 @@ class ArticleScreenViewModel( clearArticle() } - private fun updateArticlesSince() { - articlesSince.value = OffsetDateTime.now() + private fun updateArticlesSince(since: OffsetDateTime = OffsetDateTime.now()) { + articlesSince.value = since } private fun copyFolderCounts( diff --git a/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt b/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt index 650ca541..789c4859 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt @@ -10,7 +10,33 @@ sealed class NextFilter { data class FeedFilter(val feedID: String, val folderTitle: String? = null) : NextFilter() companion object { - fun find( + fun findMarkReadDestination( + filter: ArticleFilter, + folders: List, + feeds: List, + ): NextFilter? { + return when (filter) { + is ArticleFilter.Feeds -> findNextFeed(filter, folders, feeds) + is ArticleFilter.Folders -> { + val folderIndex = folders.indexOfFirst { it.title == filter.folderTitle } + + val nextFolder = folders.getOrNull(folderIndex + 1) + val nextFeed = feeds.firstOrNull() + + if (nextFolder != null) { + FolderFilter(nextFolder.title) + } else if (nextFeed != null) { + FeedFilter(feedID = nextFeed.id, folderTitle = filter.folderTitle) + } else { + null + } + } + + else -> null + } + } + + fun findSwipeDestination( filter: ArticleFilter, feeds: List, folders: List, @@ -38,34 +64,40 @@ sealed class NextFilter { FeedFilter(feedID = firstFeed.id, folderTitle = filter.folderTitle) } - is ArticleFilter.Feeds -> { - return if (filter.folderTitle == null) { - val index = feeds.indexOfFirst { it.id == filter.feedID } + is ArticleFilter.Feeds -> findNextFeed(filter, folders, feeds) + } + } - val nextFeed = feeds.getOrNull(index + 1) ?: return null + private fun findNextFeed( + filter: ArticleFilter.Feeds, + folders: List, + feeds: List + ): NextFilter? { + return if (filter.folderTitle == null) { + val index = feeds.indexOfFirst { it.id == filter.feedID } - FeedFilter(feedID = nextFeed.id, folderTitle = null) - } else { - val folderIndex = folders - .indexOfFirst { it.title == filter.folderTitle } - - val folderFeeds = folders.getOrNull(folderIndex)?.feeds.orEmpty() - - val index = folderFeeds.indexOfFirst { it.id == filter.feedID } - val nextFolderFeed = folderFeeds.getOrNull(index + 1) - val nextFolder = folders.getOrNull(folderIndex + 1) - val nextFeed = feeds.firstOrNull() - - if (nextFolderFeed != null) { - FeedFilter(feedID = nextFolderFeed.id, folderTitle = filter.folderTitle) - } else if (nextFolder != null) { - FolderFilter(nextFolder.title) - } else if (nextFeed != null) { - FeedFilter(feedID = nextFeed.id, folderTitle = null) - } else { - null - } - } + val nextFeed = feeds.getOrNull(index + 1) ?: return null + + FeedFilter(feedID = nextFeed.id, folderTitle = null) + } else { + val folderIndex = folders + .indexOfFirst { it.title == filter.folderTitle } + + val folderFeeds = folders.getOrNull(folderIndex)?.feeds.orEmpty() + + val index = folderFeeds.indexOfFirst { it.id == filter.feedID } + val nextFolderFeed = folderFeeds.getOrNull(index + 1) + val nextFolder = folders.getOrNull(folderIndex + 1) + val nextFeed = feeds.firstOrNull { it.id != filter.feedID } + + if (nextFolderFeed != null) { + FeedFilter(feedID = nextFolderFeed.id, folderTitle = filter.folderTitle) + } else if (nextFolder != null) { + FolderFilter(nextFolder.title) + } else if (nextFeed != null) { + FeedFilter(feedID = nextFeed.id, folderTitle = null) + } else { + null } } } diff --git a/capy/src/test/java/com/jocmp/capy/articles/NextFilterTest.kt b/capy/src/test/java/com/jocmp/capy/articles/NextFilterTest.kt index 38c0c61a..9d6e29e6 100644 --- a/capy/src/test/java/com/jocmp/capy/articles/NextFilterTest.kt +++ b/capy/src/test/java/com/jocmp/capy/articles/NextFilterTest.kt @@ -27,7 +27,7 @@ class NextFilterTest { val folder = Folder(title = "This Is My Next Folder") val folders = listOf(folder) - val next = NextFilter.find(filter, feeds = emptyList(), folders = folders)!! + val next = NextFilter.findSwipeDestination(filter, feeds = emptyList(), folders = folders)!! assertTrue(next is NextFilter.FolderFilter) assertEquals(actual = next.folderTitle, expected = folder.title) @@ -39,7 +39,7 @@ class NextFilterTest { val feed = feedFixture.create() val feeds = listOf(feed) - val next = NextFilter.find(filter, feeds = feeds, folders = emptyList())!! + val next = NextFilter.findSwipeDestination(filter, feeds = feeds, folders = emptyList())!! assertTrue(next is NextFilter.FeedFilter) assertEquals(actual = next.feedID, expected = feed.id) @@ -50,7 +50,7 @@ class NextFilterTest { fun `on article filter that is empty`() { val filter = ArticleFilter.Articles(articleStatus = ArticleStatus.UNREAD) - val next = NextFilter.find(filter, feeds = emptyList(), folders = emptyList()) + val next = NextFilter.findSwipeDestination(filter, feeds = emptyList(), folders = emptyList()) assertNull(next) } @@ -72,7 +72,7 @@ class NextFilterTest { ) val next = - NextFilter.find(filter, feeds = emptyList(), folders = listOf(anotherFolder, folder))!! + NextFilter.findSwipeDestination(filter, feeds = emptyList(), folders = listOf(anotherFolder, folder))!! val expectedFeed = folderFeeds.first() assertTrue(next is NextFilter.FeedFilter) @@ -92,7 +92,7 @@ class NextFilterTest { ) val next = - NextFilter.find(filter, feeds = emptyList(), folders = listOf(anotherFolder, folder)) + NextFilter.findSwipeDestination(filter, feeds = emptyList(), folders = listOf(anotherFolder, folder)) assertNull(next) } @@ -111,7 +111,7 @@ class NextFilterTest { feedStatus = ArticleStatus.UNREAD ) - val next = NextFilter.find( + val next = NextFilter.findSwipeDestination( filter, feeds = topLevelFeeds, folders = listOf(someFolder, anotherFolder) @@ -137,7 +137,7 @@ class NextFilterTest { feedStatus = ArticleStatus.UNREAD ) - val next = NextFilter.find( + val next = NextFilter.findSwipeDestination( filter, feeds = topLevelFeeds, folders = listOf(someFolder, anotherFolder) @@ -163,7 +163,7 @@ class NextFilterTest { feedStatus = ArticleStatus.UNREAD ) val next = - NextFilter.find(filter, feeds = emptyList(), folders = listOf(folder, anotherFolder))!! + NextFilter.findSwipeDestination(filter, feeds = emptyList(), folders = listOf(folder, anotherFolder))!! val expectedFeed = folderFeeds[1] assertTrue(next is NextFilter.FeedFilter) @@ -188,7 +188,7 @@ class NextFilterTest { feedStatus = ArticleStatus.UNREAD ) val next = - NextFilter.find(filter, feeds = emptyList(), folders = listOf(folder, anotherFolder))!! + NextFilter.findSwipeDestination(filter, feeds = emptyList(), folders = listOf(folder, anotherFolder))!! assertTrue(next is NextFilter.FolderFilter) assertEquals(actual = next.folderTitle, expected = anotherFolder.title) @@ -213,7 +213,7 @@ class NextFilterTest { feedStatus = ArticleStatus.UNREAD ) val next = - NextFilter.find(filter, feeds = topLevelFeeds, folders = listOf(folder))!! + NextFilter.findSwipeDestination(filter, feeds = topLevelFeeds, folders = listOf(folder))!! val expectedFeed = topLevelFeeds.first() assertTrue(next is NextFilter.FeedFilter)