diff --git a/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt b/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt index dd733424..7b158946 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt @@ -7,7 +7,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.Pager +import androidx.paging.PagingData import com.jocmp.basil.Account import com.jocmp.basil.AccountManager import com.jocmp.basil.Article @@ -19,6 +19,7 @@ import com.jocmp.basil.Folder import com.jocmp.basil.buildPager import com.jocmp.basilreader.selectAccount import com.jocmp.basilreader.selectedAccount +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -34,10 +35,11 @@ class AccountViewModel( private val filter = mutableStateOf(ArticleFilter.default()) - private val _pager = mutableStateOf(account.buildPager(filter.value)) - val pager: Pager - get() = _pager.value + private val pager = mutableStateOf(account.buildPager(filter.value)) + + val articles: Flow> + get() = pager.value.flow private val account: Account get() = accountState.value @@ -59,8 +61,7 @@ class AccountViewModel( fun selectStatus(status: ArticleStatus) { val nextFilter = filter.value.withStatus(status = status) filter.value = nextFilter - - _pager.value = account.buildPager(nextFilter) + pager.value = account.buildPager(nextFilter) } fun selectFeed(feedID: String, onComplete: () -> Unit) { @@ -95,7 +96,7 @@ class AccountViewModel( private fun selectFilter(nextFilter: ArticleFilter, onComplete: () -> Unit) { filter.value = nextFilter - _pager.value = account.buildPager(nextFilter) + pager.value = account.buildPager(nextFilter) clearArticle() diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt index a58eef3c..c70ea6ab 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt @@ -18,12 +18,18 @@ fun ArticleFilterNavigationBar( selected: ArticleStatus, onSelect: (status: ArticleStatus) -> Unit ) { + val checkedSelect = { status: ArticleStatus -> + if (selected != status) { + onSelect(status) + } + } + NavigationBar { NavigationBarItem( icon = { Icon(Icons.Filled.Favorite, contentDescription = null) }, label = { Text(stringResource(id = R.string.article_filters_starred)) }, selected = selected === ArticleStatus.STARRED, - onClick = { onSelect(ArticleStatus.STARRED) }, + onClick = { checkedSelect(ArticleStatus.STARRED) }, alwaysShowLabel = false ) NavigationBarItem( @@ -35,7 +41,7 @@ fun ArticleFilterNavigationBar( }, label = { Text(stringResource(R.string.article_filters_unread)) }, selected = selected === ArticleStatus.UNREAD, - onClick = { onSelect(ArticleStatus.UNREAD) }, + onClick = { checkedSelect(ArticleStatus.UNREAD) }, alwaysShowLabel = false ) NavigationBarItem( @@ -47,7 +53,7 @@ fun ArticleFilterNavigationBar( }, label = { Text(stringResource(R.string.article_filters_all)) }, selected = selected === ArticleStatus.ALL, - onClick = { onSelect(ArticleStatus.ALL) }, + onClick = { checkedSelect(ArticleStatus.ALL) }, alwaysShowLabel = false ) } diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt index 7d8c3608..10202294 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt @@ -8,107 +8,52 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.Button -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.paging.Pager +import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey import com.jocmp.basil.Article import com.jocmp.basil.ArticleStatus +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import java.time.format.DateTimeFormatter -@OptIn(ExperimentalMaterialApi::class) @Composable fun ArticleList( - pager: Pager, - onRefresh: suspend () -> Unit, + articles: Flow>, onSelect: suspend (articleID: String) -> Unit, - selectedStatus: ArticleStatus, - onStatusSelect: (status: ArticleStatus) -> Unit, - onDrawerNavigation: () -> Unit ) { val composableScope = rememberCoroutineScope() - val lazyPagingItems = pager.flow.collectAsLazyPagingItems() - - val refreshScope = rememberCoroutineScope() - val (refreshing, setRefreshing) = remember { mutableStateOf(false) } - - fun refresh() = refreshScope.launch { - setRefreshing(true) - onRefresh() - setRefreshing(false) - } - - val state = rememberPullRefreshState(refreshing, ::refresh) - - Scaffold( - topBar = { - TopAppBar { - Button(onClick = { onDrawerNavigation() }) { - Icon(imageVector = Icons.Filled.Menu, contentDescription = null) - } - } - }, - bottomBar = { - ArticleFilterNavigationBar( - selected = selectedStatus, - onSelect = { onStatusSelect(it) } - ) - } - ) { innerPadding -> - Box( - Modifier - .padding(innerPadding) - .pullRefresh(state) - ) { - LazyColumn(Modifier.fillMaxSize()) { - items( - count = lazyPagingItems.itemCount, - key = lazyPagingItems.itemKey { it.id } - ) { index -> - val item = lazyPagingItems[index]!! - - Row { - Box( - modifier = Modifier - .clickable { - composableScope.launch { - onSelect(item.id) - } - } - ) { - Column(Modifier.padding(8.dp)) { - Text(item.title, fontSize = 20.sp) - Text(item.arrivedAt.format(DateTimeFormatter.BASIC_ISO_DATE)) + val lazyPagingItems = articles.collectAsLazyPagingItems() + + LazyColumn(Modifier.fillMaxSize()) { + items( + count = lazyPagingItems.itemCount, + key = lazyPagingItems.itemKey { it.id } + ) { index -> + val item = lazyPagingItems[index]!! + + Row { + Box( + modifier = Modifier + .clickable { + composableScope.launch { + onSelect(item.id) } } + ) { + Column(Modifier.padding(8.dp)) { + Text(item.title, fontSize = 20.sp) + Text(item.arrivedAt.format(DateTimeFormatter.BASIC_ISO_DATE)) } } } - - PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } } } - -data class ArticleStatusNavigationItem( - val icon: Icons, - val label: String -) diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt index 0ceac3a2..a25e7180 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt @@ -1,37 +1,50 @@ package com.jocmp.basilreader.ui.articles +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.calculateListDetailPaneScaffoldState import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import com.jocmp.basil.ArticleFilter +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import com.jocmp.basilreader.ui.accounts.AccountViewModel -import com.jocmp.basilreader.ui.components.EmptyView import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel -@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterialApi::class) @Composable fun ArticleScreen( - viewModel: AccountViewModel = koinViewModel(), - onFeedAdd: () -> Unit, + viewModel: AccountViewModel = koinViewModel(), + onFeedAdd: () -> Unit, ) { val drawerState = rememberDrawerState(DrawerValue.Closed) val coroutineScope = rememberCoroutineScope() - val (destination, setDestination) = rememberSaveable { mutableStateOf(ListDetailPaneScaffoldRole.List) } - val scaffoldState = calculateListDetailPaneScaffoldState( - currentPaneDestination = destination, - scaffoldDirective = calculateArticleDirective() - ) + val (destination, setDestination) = + rememberSaveable { mutableStateOf(ListDetailPaneScaffoldRole.List) } + val scaffoldState = + calculateListDetailPaneScaffoldState( + currentPaneDestination = destination, + scaffoldDirective = calculateArticleDirective() + ) - val navigateToDetail = { - setDestination(ListDetailPaneScaffoldRole.Detail) - } + val navigateToDetail = { setDestination(ListDetailPaneScaffoldRole.Detail) } val onComplete = { coroutineScope.launch { @@ -40,45 +53,67 @@ fun ArticleScreen( } } + val refreshScope = rememberCoroutineScope() + val (refreshing, setRefreshing) = remember { mutableStateOf(false) } + + fun refresh() = + refreshScope.launch { + setRefreshing(true) + viewModel.refreshFeed() + setRefreshing(false) + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + ArticleScaffold( - drawerState = drawerState, - listDetailState = scaffoldState, - drawerPane = { - FeedList( - folders = viewModel.folders, - feeds = viewModel.feeds, - onFeedAdd = onFeedAdd, - onFolderSelect = { viewModel.selectFolder(it) { onComplete() } }, - onFeedSelect = { viewModel.selectFeed(it) { onComplete() } } - ) - }, - listPane = { - ArticleList( - pager = viewModel.pager, - onRefresh = { - viewModel.refreshFeed() - }, - onDrawerNavigation = { - coroutineScope.launch { - drawerState.open() + drawerState = drawerState, + listDetailState = scaffoldState, + drawerPane = { + FeedList( + folders = viewModel.folders, + feeds = viewModel.feeds, + onFeedAdd = onFeedAdd, + onFolderSelect = { viewModel.selectFolder(it) { onComplete() } }, + onFeedSelect = { viewModel.selectFeed(it) { onComplete() } } + ) + }, + listPane = { + Scaffold( + topBar = { + TopAppBar { + Button(onClick = { coroutineScope.launch { drawerState.open() } }) { + Icon(imageVector = Icons.Filled.Menu, contentDescription = null) + } + } + }, + bottomBar = { + ArticleFilterNavigationBar( + selected = viewModel.filterStatus, + onSelect = viewModel::selectStatus, + ) + } + ) { innerPadding -> + Box(Modifier.padding(innerPadding).pullRefresh(state)) { + ArticleList( + articles = viewModel.articles, + onSelect = { + viewModel.selectArticle(it) + navigateToDetail() + } + ) + + PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) } - }, - selectedStatus = viewModel.filterStatus, - onStatusSelect = viewModel::selectStatus, - onSelect = { - viewModel.selectArticle(it) - navigateToDetail() - } - ) - }, - detailPane = { - ArticleView( - article = viewModel.article, - onBackPressed = { - viewModel.clearArticle() - setDestination(ListDetailPaneScaffoldRole.List) } - ) - } + }, + detailPane = { + ArticleView( + article = viewModel.article, + onBackPressed = { + viewModel.clearArticle() + setDestination(ListDetailPaneScaffoldRole.List) + } + ) + } ) }