Skip to content

Commit

Permalink
Add article list pagination (#592)
Browse files Browse the repository at this point in the history
* Fix feed list bottom bar

* Extract NextFilter module

* Add tests
  • Loading branch information
jocmp authored Dec 10, 2024
1 parent 76b9f8f commit 893aeb6
Show file tree
Hide file tree
Showing 15 changed files with 496 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ class ArticleNotifications(
appPreferences.filter.set(
ArticleFilter.Feeds(
feedID,
feedStatus = ArticleStatus.UNREAD
feedStatus = ArticleStatus.UNREAD,
folderTitle = null
)
)
appPreferences.articleID.set(articleID)
Expand Down
27 changes: 12 additions & 15 deletions app/src/main/java/com/capyreader/app/ui/articles/AddFeedDialog.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.capyreader.app.ui.articles

import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.Dialog
import org.koin.compose.koinInject
Expand All @@ -12,19 +11,17 @@ fun AddFeedDialog(
onComplete: (feedID: String) -> Unit,
) {
Dialog(onDismissRequest = onCancel) {
Column {
AddFeedView(
feedChoices = viewModel.feedChoices,
onAddFeed = { url ->
viewModel.addFeed(
url = url,
onComplete = { onComplete(it.id) },
)
},
onCancel = onCancel,
loading = viewModel.loading,
error = viewModel.error
)
}
AddFeedView(
feedChoices = viewModel.feedChoices,
onAddFeed = { url ->
viewModel.addFeed(
url = url,
onComplete = { onComplete(it.id) },
)
},
onCancel = onCancel,
loading = viewModel.loading,
error = viewModel.error
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.capyreader.app.R
import com.capyreader.app.ui.components.DialogCard
import com.jocmp.capy.accounts.AddFeedResult
import com.jocmp.capy.accounts.FeedOption

Expand Down Expand Up @@ -68,9 +69,7 @@ fun AddFeedView(
}
}

Card(
shape = RoundedCornerShape(16.dp)
) {
DialogCard {
Column(Modifier.padding(top = 16.dp)) {
OutlinedTextField(
value = queryURL,
Expand Down
98 changes: 63 additions & 35 deletions app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import com.capyreader.app.ui.articles.detail.CapyPlaceholder
import com.capyreader.app.ui.articles.detail.resetScrollBehaviorListener
import com.capyreader.app.ui.articles.list.EmptyOnboardingView
import com.capyreader.app.ui.articles.list.FeedListTopBar
import com.capyreader.app.ui.articles.list.PullToNextFeedBox
import com.capyreader.app.ui.articles.media.ArticleMediaView
import com.capyreader.app.ui.components.ArticleSearch
import com.capyreader.app.ui.components.rememberWebViewState
Expand Down Expand Up @@ -79,7 +80,7 @@ fun ArticleLayout(
refreshInterval: RefreshInterval,
onFeedRefresh: (completion: () -> Unit) -> Unit,
onSelectFolder: (folderTitle: String) -> Unit,
onSelectFeed: suspend (feedID: String) -> Unit,
onSelectFeed: (feedID: String, folderTitle: String?) -> Unit,
onSelectArticleFilter: () -> Unit,
onSelectStatus: (status: ArticleStatus) -> Unit,
onSelectArticle: (articleID: String) -> Unit,
Expand All @@ -88,10 +89,12 @@ fun ArticleLayout(
onToggleArticleRead: () -> Unit,
onToggleArticleStar: () -> Unit,
onMarkAllRead: (range: MarkRead) -> Unit,
onRequestNextFeed: () -> Unit,
onRemoveFeed: (feedID: String, onSuccess: () -> Unit, onFailure: () -> Unit) -> Unit,
drawerValue: DrawerValue = DrawerValue.Closed,
showUnauthorizedMessage: Boolean,
onUnauthorizedDismissRequest: () -> Unit
onUnauthorizedDismissRequest: () -> Unit,
canSwipeToNextFeed: Boolean,
) {
val skipInitialRefresh = refreshInterval == RefreshInterval.MANUALLY_ONLY

Expand Down Expand Up @@ -142,6 +145,37 @@ fun ArticleLayout(
scrollBehavior = scrollBehavior
)

suspend fun openNextStatus(action: suspend () -> Unit) {
listVisible = false
delay(200)
action()
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.List)
resetScrollBehaviorOffset()

coroutineScope.launch {
delay(500)
if (!listVisible) {
listVisible = true
}
}
}

fun requestNextFeed() {
coroutineScope.launchUI {
openNextStatus {
onRequestNextFeed()
}
}
}

fun markAllRead(range: MarkRead) {
if (range is MarkRead.All && filter !is ArticleFilter.Articles && canSwipeToNextFeed) {
requestNextFeed()
}

onMarkAllRead(range)
}

val scrollToTop = {
coroutineScope.launch {
listState.scrollToItem(0)
Expand All @@ -167,21 +201,6 @@ fun ArticleLayout(
}
}

suspend fun openNextStatus(action: suspend () -> Unit) {
listVisible = false
delay(200)
action()
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.List)
resetScrollBehaviorOffset()

coroutineScope.launch {
delay(500)
if (!listVisible) {
listVisible = true
}
}
}

fun openNextList(action: suspend () -> Unit) {
coroutineScope.launchUI {
openNextStatus(action)
Expand Down Expand Up @@ -217,7 +236,7 @@ fun ArticleLayout(

val onFeedAdded = { feedID: String ->
coroutineScope.launch {
openNextList { onSelectFeed(feedID) }
openNextList { onSelectFeed(feedID, null) }

showSnackbar(addFeedSuccessMessage)
}
Expand Down Expand Up @@ -251,10 +270,10 @@ fun ArticleLayout(
closeDrawer()
}
},
onSelectFeed = {
onSelectFeed = { feed, folderTitle ->
coroutineScope.launch {
if (!filter.isFeedSelected(it)) {
openNextList { onSelectFeed(it.id) }
if (!filter.isFeedSelected(feed)) {
openNextList { onSelectFeed(feed.id, folderTitle) }
} else {
closeDrawer()
}
Expand Down Expand Up @@ -313,7 +332,9 @@ fun ArticleLayout(
scrollToTop()
},
scrollBehavior = scrollBehavior,
onMarkAllRead = onMarkAllRead,
onMarkAllRead = {
markAllRead(it)
},
search = search,
filter = filter,
currentFeed = currentFeed,
Expand All @@ -339,19 +360,26 @@ fun ArticleLayout(
)
}
} else {
AnimatedVisibility(
listVisible,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.fillMaxSize(),
PullToNextFeedBox(
enabled = canSwipeToNextFeed,
onRequestNext = {
requestNextFeed()
},
) {
ArticleList(
articles = pagingArticles,
selectedArticleKey = article?.id,
listState = listState,
onMarkAllRead = onMarkAllRead,
onSelect = { selectArticle(it) },
)
AnimatedVisibility(
listVisible,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.fillMaxSize(),
) {
ArticleList(
articles = pagingArticles,
selectedArticleKey = article?.id,
listState = listState,
onMarkAllRead = onMarkAllRead,
onSelect = { selectArticle(it) },
)
}
}
}
}
Expand Down Expand Up @@ -451,7 +479,7 @@ fun ArticleLayout(
toggleDrawer()
}

LaunchedEffect(pagingArticles.itemCount) {
LaunchedEffect(pagingArticles.itemCount, filter) {
if (!listVisible) {
listState.scrollToItem(0)
listVisible = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculateListDetailPaneScaffoldMotion
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberDrawerState
Expand All @@ -27,7 +29,6 @@ import com.capyreader.app.common.AppPreferences
import com.capyreader.app.common.LayoutPreference
import com.capyreader.app.common.asState
import com.capyreader.app.ui.FadePaneMotion
import com.capyreader.app.ui.components.safeEdgePadding
import com.capyreader.app.ui.isAtMostMedium
import com.capyreader.app.ui.theme.CapyTheme
import org.koin.compose.koinInject
Expand Down Expand Up @@ -59,8 +60,7 @@ fun ArticleScaffold(
},
) {
ListDetailPaneScaffold(
modifier = Modifier.safeEdgePadding(),
directive = scaffoldNavigator.scaffoldDirective.copy(maxHorizontalPartitions = 1),
directive = scaffoldNavigator.scaffoldDirective,
value = scaffoldNavigator.scaffoldValue,
paneMotions = paneMotions,
listPane = {
Expand Down Expand Up @@ -94,8 +94,11 @@ fun rememberArticleScaffoldNavigator(appPreferences: AppPreferences = koinInject
)
)
}
val directive = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())

return rememberListDetailPaneScaffoldNavigator()
return rememberListDetailPaneScaffoldNavigator(
scaffoldDirective = directive.copy(horizontalPartitionSpacerSize = 0.dp)
)
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ fun ArticleScreen(
val filter by viewModel.filter.collectAsStateWithLifecycle(appPreferences.filter.get())
val searchQuery by viewModel.searchQuery.collectAsState(initial = null)
val articles = viewModel.articles.collectAsLazyPagingItems()
val nextFilter by viewModel.nextFilter.collectAsStateWithLifecycle(initialValue = null)
val canSwipeToNextFeed = nextFilter != null

val fullContent = rememberFullContent(viewModel)
val articleActions = rememberArticleActions(viewModel)
Expand Down Expand Up @@ -64,6 +66,8 @@ fun ArticleScreen(
onRemoveFeed = viewModel::removeFeed,
showUnauthorizedMessage = viewModel.showUnauthorizedMessage,
onUnauthorizedDismissRequest = viewModel::dismissUnauthorizedMessage,
onRequestNextFeed = viewModel::requestNextFeed,
canSwipeToNextFeed = canSwipeToNextFeed,
search = ArticleSearch(
query = searchQuery,
clear = { viewModel.clearSearch() },
Expand Down
Loading

0 comments on commit 893aeb6

Please sign in to comment.