From f4828ac01a3d9e53034272a60f4e0635588e4484 Mon Sep 17 00:00:00 2001 From: Ash Date: Sat, 19 Mar 2022 21:10:22 +0800 Subject: [PATCH] Refactor Material You design for FlowPage --- app/src/main/java/me/ash/reader/NumberExt.kt | 26 +-- .../me/ash/reader/data/constant/Filter.kt | 12 +- .../java/me/ash/reader/data/feed/FeedDao.kt | 9 + .../java/me/ash/reader/data/group/GroupDao.kt | 8 + .../data/repository/AbstractRssRepository.kt | 7 +- .../data/repository/AccountRepository.kt | 14 ++ .../data/repository/FeverRssRepository.kt | 7 +- .../reader/data/repository/OpmlRepository.kt | 4 + .../ash/reader/data/repository/RssHelper.kt | 67 ++++--- .../me/ash/reader/ui/page/home/HomePage.kt | 31 +-- .../ui/page/home/article/ArticleItem.kt | 143 -------------- .../ui/page/home/article/ArticlePage.kt | 145 --------------- .../ui/page/home/article/ArticlePageTopBar.kt | 59 ------ .../page/home/feeds/{Feed.kt => FeedItem.kt} | 2 +- .../reader/ui/page/home/feeds/FeedsPage.kt | 176 +++++++++--------- .../ui/page/home/feeds/FeedsViewModel.kt | 13 +- .../home/feeds/{Group.kt => GroupItem.kt} | 10 +- .../home/feeds/subscribe/ResultViewPage.kt | 82 +++----- .../home/feeds/subscribe/SubscribeDialog.kt | 8 + .../feeds/subscribe/SubscribeViewModel.kt | 8 + .../reader/ui/page/home/flow/ArticleItem.kt | 82 ++++++++ .../reader/ui/page/home/flow/ArticleList.kt | 60 ++++++ .../ash/reader/ui/page/home/flow/FlowPage.kt | 121 ++++++++++++ .../FlowViewModel.kt} | 52 ++---- .../StickyHeader.kt} | 20 +- .../java/me/ash/reader/ui/widget/Banner.kt | 2 +- .../me/ash/reader/ui/widget/SelectionChip.kt | 97 +++++++++- .../java/me/ash/reader/ui/widget/SubTitle.kt | 4 +- 28 files changed, 627 insertions(+), 642 deletions(-) delete mode 100644 app/src/main/java/me/ash/reader/ui/page/home/article/ArticleItem.kt delete mode 100644 app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePage.kt delete mode 100644 app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt rename app/src/main/java/me/ash/reader/ui/page/home/feeds/{Feed.kt => FeedItem.kt} (99%) rename app/src/main/java/me/ash/reader/ui/page/home/feeds/{Group.kt => GroupItem.kt} (93%) create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt rename app/src/main/java/me/ash/reader/ui/page/home/{article/ArticleViewModel.kt => flow/FlowViewModel.kt} (63%) rename app/src/main/java/me/ash/reader/ui/page/home/{article/ArticleDateHeader.kt => flow/StickyHeader.kt} (56%) diff --git a/app/src/main/java/me/ash/reader/NumberExt.kt b/app/src/main/java/me/ash/reader/NumberExt.kt index 5bc74e5..d5e5438 100644 --- a/app/src/main/java/me/ash/reader/NumberExt.kt +++ b/app/src/main/java/me/ash/reader/NumberExt.kt @@ -1,27 +1,3 @@ package me.ash.reader -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.runtime.toMutableStateList - -fun Int.positive() = if (this < 0) 0 else this -fun Int.finitelyLarge(value: Int) = if (this > value) value else this -fun Int.finitelySmall(value: Int) = if (this < value) value else this - -fun Float.positive() = if (this < 0) 0f else this -fun Float.finitelyLarge(value: Float) = if (this > value) value else this -fun Float.finitelySmall(value: Float) = if (this < value) value else this - -@Composable -fun rememberMutableStateListOf(vararg elements: T): SnapshotStateList { - return rememberSaveable( - saver = listSaver( - save = { it.toList() }, - restore = { it.toMutableStateList() } - ) - ) { - elements.toMutableList().toMutableStateList() - } -} \ No newline at end of file +fun Int.spacerDollar(str: Any): String = "$this$$str" \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/constant/Filter.kt b/app/src/main/java/me/ash/reader/data/constant/Filter.kt index d00d48c..96ebbc3 100644 --- a/app/src/main/java/me/ash/reader/data/constant/Filter.kt +++ b/app/src/main/java/me/ash/reader/data/constant/Filter.kt @@ -8,29 +8,33 @@ import androidx.compose.ui.graphics.vector.ImageVector class Filter( var index: Int, - var title: String, + var name: String, var description: String, var important: Int, var icon: ImageVector, ) { + fun isStarred(): Boolean = this == Starred + fun isUnread(): Boolean = this == Unread + fun isAll(): Boolean = this == All + companion object { val Starred = Filter( index = 0, - title = "Starred", + name = "Starred", description = " Starred Items", important = 13, icon = Icons.Rounded.StarOutline, ) val Unread = Filter( index = 1, - title = "Unread", + name = "Unread", description = " Unread Items", important = 666, icon = Icons.Outlined.FiberManualRecord, ) val All = Filter( index = 2, - title = "All", + name = "All", description = " Unread Items", important = 666, icon = Icons.Rounded.Subject, diff --git a/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt b/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt index c3e6ac0..abba6d0 100644 --- a/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt +++ b/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt @@ -12,6 +12,15 @@ interface FeedDao { ) suspend fun queryAll(accountId: Int): List + @Query( + """ + SELECT * FROM feed + WHERE accountId = :accountId + and url = :url + """ + ) + fun queryByLink(accountId: Int, url: String): List + @Insert suspend fun insert(feed: Feed): Long diff --git a/app/src/main/java/me/ash/reader/data/group/GroupDao.kt b/app/src/main/java/me/ash/reader/data/group/GroupDao.kt index 1a7443e..5da24e0 100644 --- a/app/src/main/java/me/ash/reader/data/group/GroupDao.kt +++ b/app/src/main/java/me/ash/reader/data/group/GroupDao.kt @@ -22,6 +22,14 @@ interface GroupDao { ) fun queryAllGroup(accountId: Int): Flow> + @Query( + """ + SELECT * FROM `group` + WHERE accountId = :accountId + """ + ) + suspend fun queryAll(accountId: Int): List + @Insert suspend fun insert(group: Group): Long diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index 430571b..91b0751 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -108,7 +108,7 @@ abstract class AbstractRssRepository constructor( isStarred: Boolean = false, isUnread: Boolean = false, ): Flow> { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 + val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! Log.i( "RLog", "pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}" @@ -126,6 +126,11 @@ abstract class AbstractRssRepository constructor( return articleDao.queryById(id) } + fun isExist(url: String): Boolean { + val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! + return feedDao.queryByLink(accountId, url).isNotEmpty() + } + fun peekWork(): String { return workManager.getWorkInfosByTag("sync").get().size.toString() } diff --git a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt index 51c6018..25c1bfc 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt @@ -5,14 +5,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext import me.ash.reader.DataStoreKeys import me.ash.reader.data.account.Account import me.ash.reader.data.account.AccountDao +import me.ash.reader.data.group.Group +import me.ash.reader.data.group.GroupDao import me.ash.reader.dataStore import me.ash.reader.get +import me.ash.reader.spacerDollar import javax.inject.Inject class AccountRepository @Inject constructor( @ApplicationContext private val context: Context, private val accountDao: AccountDao, + private val groupDao: GroupDao, ) { suspend fun getCurrentAccount(): Account? { @@ -30,6 +34,16 @@ class AccountRepository @Inject constructor( type = Account.Type.LOCAL, ).apply { id = accountDao.insert(this).toInt() + }.also { + if (groupDao.queryAll(it.id!!).isEmpty()) { + groupDao.insert( + Group( + id = it.id!!.spacerDollar("0"), + name = "默认", + accountId = it.id!!, + ) + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt index 90de987..d0c505e 100644 --- a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt @@ -20,6 +20,7 @@ import me.ash.reader.data.source.FeverApiDataSource import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.dataStore import me.ash.reader.get +import me.ash.reader.spacerDollar import net.dankito.readability4j.extended.Readability4JExtended import java.util.* import javax.inject.Inject @@ -82,7 +83,7 @@ class FeverRssRepository @Inject constructor( feverGroupsBody.groups.forEach { groupDao.insert( Group( - id = it.id.toString(), + id = accountId.spacerDollar(it.id), name = it.title, accountId = accountId, ) @@ -99,7 +100,7 @@ class FeverRssRepository @Inject constructor( } val feeds = feverFeeds.map { Feed( - id = it.id.toString(), + id = accountId.spacerDollar(it.id), name = it.title, url = it.url, groupId = feverFeedsGroupsMap[it.id].toString(), @@ -116,7 +117,7 @@ class FeverRssRepository @Inject constructor( .forEach { articles.add( Article( - id = it.id, + id = accountId.spacerDollar(it.id), date = Date(it.created_on_time * 1000), title = it.title, author = it.author, diff --git a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt index 1b3e648..9934295 100644 --- a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt @@ -10,6 +10,7 @@ import javax.inject.Inject class OpmlRepository @Inject constructor( private val groupDao: GroupDao, private val feedDao: FeedDao, + private val rssRepository: RssRepository, private val opmlLocalDataSource: OpmlLocalDataSource ) { suspend fun saveToDatabase(inputStream: InputStream) { @@ -18,6 +19,9 @@ class OpmlRepository @Inject constructor( groupWithFeedList.forEach { groupWithFeed -> groupDao.insert(groupWithFeed.group) groupWithFeed.feeds.forEach { it.groupId = groupWithFeed.group.id } + groupWithFeed.feeds.removeIf { + rssRepository.get().isExist(it.url) + } feedDao.insertList(groupWithFeed.feeds) } } catch (e: Exception) { diff --git a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt index 2a62ec8..6b239fc 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt @@ -11,6 +11,7 @@ import me.ash.reader.data.feed.FeedWithArticle import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.dataStore import me.ash.reader.get +import me.ash.reader.spacerDollar import net.dankito.readability4j.Readability4J import net.dankito.readability4j.extended.Readability4JExtended import okhttp3.* @@ -28,17 +29,17 @@ class RssHelper @Inject constructor( val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val parseRss = rssNetworkDataSource.parseRss(feedLink) val feed = Feed( - id = UUID.randomUUID().toString(), + id = accountId.spacerDollar(UUID.randomUUID().toString()), name = parseRss.title!!, url = feedLink, - groupId = UUID.randomUUID().toString(), + groupId = "", accountId = accountId, ) val articles = mutableListOf
() parseRss.items.forEach { articles.add( Article( - id = UUID.randomUUID().toString(), + id = accountId.spacerDollar(UUID.randomUUID().toString()), accountId = accountId, feedId = feed.id, date = Date(it.publishDate.toString()), @@ -101,7 +102,7 @@ class RssHelper @Inject constructor( Log.i("RLog", "request rss ${feed.name}: ${it.title}") a.add( Article( - id = UUID.randomUUID().toString(), + id = accountId.spacerDollar(UUID.randomUUID().toString()), accountId = accountId, feedId = feed.id, date = Date(it.publishDate.toString()), @@ -126,35 +127,39 @@ class RssHelper @Inject constructor( feed: Feed, articleLink: String?, ) { - if (articleLink == null) return - val execute = OkHttpClient() - .newCall(Request.Builder().url(articleLink).build()) - .execute() - val content = execute.body?.string() - val regex = - Regex(""" Unit, -) { - if (articleWithFeed == null) return - Column( - modifier = modifier - .paddingFixedHorizontal( - top = if (index == 0) 8.dp else 0.dp, - bottom = 8.dp - ) - .roundClick { - articleOnClick(articleWithFeed) - } - .alpha( - if (isStarredFilter || articleWithFeed.article.isUnread) { - 1f - } else { - 0.7f - } - ) - ) { - Column(modifier = modifier.padding(10.dp)) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - modifier = Modifier.padding(start = 32.dp), - text = articleWithFeed.feed.name, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = if (isStarredFilter || articleWithFeed.article.isUnread) { - MaterialTheme.colorScheme.tertiary - } else { - MaterialTheme.colorScheme.outline - }, - ) - Text( - text = articleWithFeed.article.date.toString( - DateTimeExt.HH_MM - ), - fontSize = 13.sp, - color = MaterialTheme.colorScheme.outline - ) - } - Spacer(modifier = modifier.height(1.dp)) - Row { - if (true) { - Box( - modifier = Modifier - .padding(top = 3.dp) - .size(24.dp) - .border( - 2.dp, - MaterialTheme.colorScheme.inverseOnSurface, - RoundedCornerShape(4.dp) - ), - ) { - if (articleWithFeed.feed.icon == null) { - Icon( - painter = painterResource(id = R.drawable.default_folder), - contentDescription = "icon", - modifier = modifier - .fillMaxSize() - .padding(2.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } else { - Image( - painter = BitmapPainter( - BitmapFactory.decodeByteArray( - articleWithFeed.feed.icon, - 0, - articleWithFeed.feed.icon!!.size - ).asImageBitmap() - ), - contentDescription = "icon", - modifier = modifier - .fillMaxSize() - .padding(2.dp), - ) - } - } - Spacer(modifier = Modifier.width(8.dp)) - } - Column { - Text( - text = articleWithFeed.article.title, - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = if (isStarredFilter || articleWithFeed.article.isUnread) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.outline - }, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = modifier.height(1.dp)) - Text( - text = articleWithFeed.article.shortDescription, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.outline, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePage.kt deleted file mode 100644 index 428a50a..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePage.kt +++ /dev/null @@ -1,145 +0,0 @@ -package me.ash.reader.ui.page.home.article - -import android.util.Log -import android.widget.Toast -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavHostController -import androidx.paging.compose.collectAsLazyPagingItems -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.flow.collect -import me.ash.reader.DateTimeExt -import me.ash.reader.DateTimeExt.toString -import me.ash.reader.data.article.ArticleWithFeed -import me.ash.reader.data.constant.Filter -import me.ash.reader.ui.extension.collectAsStateValue -import me.ash.reader.ui.page.home.HomeViewAction -import me.ash.reader.ui.page.home.HomeViewModel -import me.ash.reader.ui.widget.AnimateLazyColumn -import me.ash.reader.ui.widget.TopTitleBox - -@OptIn(ExperimentalFoundationApi::class) -@DelicateCoroutinesApi -@Composable -fun ArticlePage( - navController: NavHostController, - modifier: Modifier = Modifier, - homeViewModel: HomeViewModel = hiltViewModel(), - viewModel: ArticleViewModel = hiltViewModel(), - BackOnClick: () -> Unit, - articleOnClick: (ArticleWithFeed) -> Unit, -) { - val context = LocalContext.current - val viewState = viewModel.viewState.collectAsStateValue() - val filterState = homeViewModel.filterState.collectAsStateValue() - val pagingItems = viewState.pagingData?.collectAsLazyPagingItems() - val refreshState = rememberSwipeRefreshState(isRefreshing = viewState.isRefreshing) - val syncState = homeViewModel.syncState.collectAsStateValue() - - LaunchedEffect(homeViewModel.filterState) { - homeViewModel.filterState.collect { state -> - Log.i("RLog", "LaunchedEffect filterState: ") - viewModel.dispatch( - ArticleViewAction.FetchData( - groupId = state.group?.id, - feedId = state.feed?.id, - isStarred = state.filter.let { it != Filter.All && it == Filter.Starred }, - isUnread = state.filter.let { it != Filter.All && it == Filter.Unread }, - ) - ) - } - } - - SwipeRefresh( - state = refreshState, - refreshTriggerDistance = 100.dp, - onRefresh = { - if (syncState.isSyncing) return@SwipeRefresh - homeViewModel.dispatch(HomeViewAction.Sync()) - } - ) { - Box { - TopTitleBox( - title = when { - filterState.group != null -> filterState.group.name - filterState.feed != null -> filterState.feed.name - else -> filterState.filter.title - }, - description = if (syncState.isSyncing) { - "Syncing (${syncState.syncedCount}/${syncState.feedCount}) : ${syncState.currentFeedName}" - } else { - "${viewState.filterImportant}${filterState.filter.description}" - }, - listState = viewState.listState, - startOffset = Offset(if (true) 52f else 20f, 72f), - startHeight = 50f, - startTitleFontSize = 24f, - startDescriptionFontSize = 14f, - ) { - viewModel.dispatch(ArticleViewAction.ScrollToItem(0)) - } - Column { - ArticlePageTopBar( - backOnClick = BackOnClick, - readAllOnClick = { - viewModel.dispatch(ArticleViewAction.PeekSyncWork) - Toast.makeText(context, viewState.syncWorkInfo, Toast.LENGTH_LONG) - .show() - }, - searchOnClick = { - - }, - ) - - Column(modifier = Modifier.weight(1f)) { - AnimateLazyColumn( - state = viewState.listState, - reference = filterState.filter, - ) { - if (pagingItems == null) return@AnimateLazyColumn - var lastItemDay: String? = null - item { - Spacer(modifier = Modifier.height(74.dp)) - } - for (itemIndex in 0 until pagingItems.itemCount) { - val currentItem = pagingItems.peek(itemIndex) - val currentItemDay = - currentItem?.article?.date?.toString(DateTimeExt.YYYY_MM_DD, true) - ?: "null" - if (lastItemDay != currentItemDay) { - if (itemIndex != 0) { - item { Spacer(modifier = Modifier.height(40.dp)) } - } - stickyHeader { - ArticleDateHeader(currentItemDay, true) - } - } - item { - ArticleItem( - modifier = modifier, - articleWithFeed = pagingItems[itemIndex], - isStarredFilter = filterState.filter == Filter.Starred, - index = itemIndex, - articleOnClick = articleOnClick, - ) - } - lastItemDay = currentItemDay - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt deleted file mode 100644 index 05de02b..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticlePageTopBar.kt +++ /dev/null @@ -1,59 +0,0 @@ -package me.ash.reader.ui.page.home.article - -import android.view.HapticFeedbackConstants -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBackIosNew -import androidx.compose.material.icons.rounded.DoneAll -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SmallTopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalView - -@Composable -fun ArticlePageTopBar( - backOnClick: () -> Unit = {}, - readAllOnClick: () -> Unit = {}, - searchOnClick: () -> Unit = {}, -) { - val view = LocalView.current - SmallTopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - backOnClick() - }) { - Icon( - imageVector = Icons.Rounded.ArrowBackIosNew, - contentDescription = "Back", - tint = MaterialTheme.colorScheme.primary - ) - } - }, - actions = { - IconButton(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - readAllOnClick() - }) { - Icon( - imageVector = Icons.Rounded.DoneAll, - contentDescription = "Done All", - tint = MaterialTheme.colorScheme.primary - ) - } - IconButton(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - searchOnClick() - }) { - Icon( - imageVector = Icons.Rounded.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.primary - ) - } - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/Feed.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt similarity index 99% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/Feed.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt index 79e027e..54ad4dc 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/Feed.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable -fun Feed( +fun FeedItem( modifier: Modifier = Modifier, name: String, important: Int, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index ec1be88..edf6afe 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -2,12 +2,11 @@ package me.ash.reader.ui.page.home.feeds import androidx.compose.animation.core.* import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.KeyboardArrowRight import androidx.compose.material.icons.rounded.Add @@ -19,15 +18,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.draw.rotate import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import kotlinx.coroutines.flow.collect -import me.ash.reader.data.constant.Filter import me.ash.reader.data.constant.Symbol import me.ash.reader.ui.extension.collectAsStateValue -import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog @@ -47,6 +44,7 @@ fun FeedsPage( ) { val scope = rememberCoroutineScope() val viewState = viewModel.viewState.collectAsStateValue() + val filterState = homeViewModel.filterState.collectAsStateValue() val syncState = homeViewModel.syncState.collectAsStateValue() val infiniteTransition = rememberInfiniteTransition() @@ -65,10 +63,7 @@ fun FeedsPage( LaunchedEffect(homeViewModel.filterState) { homeViewModel.filterState.collect { state -> viewModel.dispatch( - FeedsViewAction.FetchData( - isStarred = state.filter.let { it != Filter.All && it == Filter.Starred }, - isUnread = state.filter.let { it != Filter.All && it == Filter.Unread }, - ) + FeedsViewAction.FetchData(state) ) } } @@ -93,9 +88,7 @@ fun FeedsPage( homeViewModel.dispatch(HomeViewAction.Sync()) }) { Icon( - modifier = Modifier.graphicsLayer { - rotationZ = if (syncState.isSyncing) angle else 0f - }, + modifier = Modifier.rotate(if (syncState.isSyncing) angle else 0f), imageVector = Icons.Rounded.Refresh, contentDescription = "Refresh", tint = MaterialTheme.colorScheme.onSurface, @@ -119,79 +112,96 @@ fun FeedsPage( viewModel.dispatch(FeedsViewAction.AddFromFile(it)) }, ) - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - Text( - modifier = Modifier.padding( - start = 24.dp, - top = 48.dp, - end = 24.dp, - bottom = 24.dp - ), - text = viewState.account?.name ?: Symbol.Unknown, - style = MaterialTheme.typography.displaySmall, - color = MaterialTheme.colorScheme.onSurface, - ) - Banner( - title = viewState.filter.title, - desc = "${viewState.filter.important}${viewState.filter.description}", - icon = viewState.filter.icon, - action = { - Icon( - imageVector = Icons.Outlined.KeyboardArrowRight, - contentDescription = "Goto", - tint = MaterialTheme.colorScheme.onSurface, + LazyColumn { + item { + Text( + modifier = Modifier.padding( + start = 24.dp, + top = 48.dp, + end = 24.dp, + bottom = 24.dp + ), + text = viewState.account?.name ?: Symbol.Unknown, + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onSurface, + ) + } + item { + Banner( + title = viewState.filter.name, + desc = "${viewState.filter.important}${viewState.filter.description}", + icon = viewState.filter.icon, + action = { + Icon( + imageVector = Icons.Outlined.KeyboardArrowRight, + contentDescription = "Goto", + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + ) { + homeViewModel.dispatch( + HomeViewAction.ChangeFilter( + filterState.copy( + group = null, + feed = null + ) + ) ) - }, - ) - Spacer(modifier = Modifier.height(24.dp)) - Subtitle( - modifier = Modifier.padding(start = 4.dp), - text = "Feeds" - ) - Spacer(modifier = Modifier.height(8.dp)) - Column { - viewState.groupWithFeedList.forEachIndexed { index, groupWithFeed -> - Group( - text = groupWithFeed.group.name, - feeds = groupWithFeed.feeds, - groupOnClick = { - homeViewModel.dispatch( - HomeViewAction.ChangeFilter( - FilterState( - group = groupWithFeed.group, - feed = null, - ) - ) - ) - homeViewModel.dispatch( - HomeViewAction.ScrollToPage( - scope = scope, - targetPage = 1, - ) - ) - }, - feedOnClick = { feed -> - homeViewModel.dispatch( - HomeViewAction.ChangeFilter( - FilterState( - group = null, - feed = feed, - ) - ) - ) - homeViewModel.dispatch( - HomeViewAction.ScrollToPage( - scope = scope, - targetPage = 1, - ) - ) - } + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + ) ) - if (index != viewState.groupWithFeedList.lastIndex) { - Spacer(modifier = Modifier.height(8.dp)) + } + } + item { + Spacer(modifier = Modifier.height(24.dp)) + Subtitle( + modifier = Modifier.padding(start = 28.dp), + text = "Feeds" + ) + Spacer(modifier = Modifier.height(8.dp)) + } + itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed -> + GroupItem( + text = groupWithFeed.group.name, + feeds = groupWithFeed.feeds, + groupOnClick = { + homeViewModel.dispatch( + HomeViewAction.ChangeFilter( + filterState.copy( + group = groupWithFeed.group, + feed = null + ) + ) + ) + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + ) + ) + }, + feedOnClick = { feed -> + homeViewModel.dispatch( + HomeViewAction.ChangeFilter( + filterState.copy( + group = null, + feed = feed + ) + ) + ) + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + ) + ) } + ) + if (index != viewState.groupWithFeedList.lastIndex) { + Spacer(modifier = Modifier.height(8.dp)) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt index 620d2c4..892006a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt @@ -14,6 +14,7 @@ import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.data.repository.AccountRepository import me.ash.reader.data.repository.OpmlRepository import me.ash.reader.data.repository.RssRepository +import me.ash.reader.ui.page.home.FilterState import java.io.InputStream import javax.inject.Inject @@ -29,7 +30,7 @@ class FeedsViewModel @Inject constructor( fun dispatch(action: FeedsViewAction) { when (action) { is FeedsViewAction.FetchAccount -> fetchAccount(action.callback) - is FeedsViewAction.FetchData -> fetchData(action.isStarred, action.isUnread) + is FeedsViewAction.FetchData -> fetchData(action.filterState) is FeedsViewAction.AddFromFile -> addFromFile(action.inputStream) is FeedsViewAction.ScrollToItem -> scrollToItem(action.index) } @@ -53,9 +54,12 @@ class FeedsViewModel @Inject constructor( } } - private fun fetchData(isStarred: Boolean, isUnread: Boolean) { + private fun fetchData(filterState: FilterState) { viewModelScope.launch(Dispatchers.IO) { - pullFeeds(isStarred, isUnread) + pullFeeds( + isStarred = filterState.filter.isStarred(), + isUnread = filterState.filter.isUnread(), + ) _viewState } } @@ -135,8 +139,7 @@ data class FeedsViewState( sealed class FeedsViewAction { data class FetchData( - val isStarred: Boolean, - val isUnread: Boolean, + val filterState: FilterState, ) : FeedsViewAction() data class FetchAccount( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/Group.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt similarity index 93% rename from app/src/main/java/me/ash/reader/ui/page/home/feeds/Group.kt rename to app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt index 07d660d..13a0cea 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/Group.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp import me.ash.reader.data.feed.Feed @Composable -fun Group( +fun GroupItem( modifier: Modifier = Modifier, text: String, feeds: List, @@ -37,7 +37,7 @@ fun Group( .clip(RoundedCornerShape(32.dp)) .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f)) .clickable { groupOnClick() } - .padding(top = 22.dp, bottom = if (expanded) 14.dp else 22.dp) + .padding(vertical = 22.dp) ) { Row( modifier = modifier.fillMaxWidth(), @@ -76,9 +76,11 @@ fun Group( exit = fadeOut() + shrinkVertically(), ) { Column { - Spacer(modifier = Modifier.height(16.dp)) + if (feeds.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + } feeds.forEach { feed -> - Feed( + FeedItem( modifier = Modifier.padding(horizontal = 20.dp), name = feed.name, important = feed.important ?: 0, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt index 4386aca..20777fb 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt @@ -2,9 +2,9 @@ package me.ash.reader.ui.page.home.feeds.subscribe import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.outlined.Notifications @@ -13,14 +13,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.MainAxisAlignment import me.ash.reader.data.group.Group import me.ash.reader.ui.widget.SelectionChip +import me.ash.reader.ui.widget.SelectionEditorChip +import me.ash.reader.ui.widget.Subtitle @Composable fun ResultViewPage( @@ -34,7 +33,9 @@ fun ResultViewPage( groupOnClick: (groupId: String) -> Unit = {}, onKeyboardAction: () -> Unit = {}, ) { - Column { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { Link( text = link ) @@ -82,11 +83,7 @@ private fun Preset( notificationPresetOnClick: () -> Unit = {}, fullContentParsePresetOnClick: () -> Unit = {}, ) { - Text( - text = "预设", - color = MaterialTheme.colorScheme.primary, - fontSize = 14.sp, - ) + Subtitle(text = "预设") Spacer(modifier = Modifier.height(10.dp)) FlowRow( mainAxisAlignment = MainAxisAlignment.Start, @@ -94,38 +91,36 @@ private fun Preset( mainAxisSpacing = 10.dp, ) { SelectionChip( + modifier = Modifier.animateContentSize(), + content = "接收通知", selected = selectedNotificationPreset, selectedIcon = { Icon( imageVector = Icons.Outlined.Notifications, contentDescription = "Check", - modifier = Modifier.size(20.dp) + modifier = Modifier + .padding(start = 8.dp) + .size(18.dp), ) }, - onClick = notificationPresetOnClick, ) { - Text( - text = "接收通知", - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - ) + notificationPresetOnClick() } SelectionChip( + modifier = Modifier.animateContentSize(), + content = "全文解析", selected = selectedFullContentParsePreset, selectedIcon = { Icon( imageVector = Icons.Outlined.Article, contentDescription = "Check", - modifier = Modifier.size(20.dp) + modifier = Modifier + .padding(start = 8.dp) + .size(18.dp), ) }, - onClick = fullContentParsePresetOnClick, ) { - Text( - text = "全文解析", - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - ) + fullContentParsePresetOnClick() } } } @@ -137,11 +132,7 @@ private fun AddToGroup( groupOnClick: (groupId: String) -> Unit = {}, onKeyboardAction: () -> Unit = {}, ) { - Text( - text = "添加到组", - color = MaterialTheme.colorScheme.primary, - fontSize = 14.sp, - ) + Subtitle(text = "添加到组") Spacer(modifier = Modifier.height(10.dp)) FlowRow( mainAxisAlignment = MainAxisAlignment.Start, @@ -151,37 +142,20 @@ private fun AddToGroup( groups.forEach { SelectionChip( modifier = Modifier.animateContentSize(), + content = it.name, selected = it.id == selectedGroupId, - onClick = { groupOnClick(it.id) }, ) { - Text( - text = it.name, - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - ) + groupOnClick(it.id) } } - SelectionChip( + SelectionEditorChip( + modifier = Modifier.animateContentSize(), + content = "新建分组", selected = false, - onClick = { /*TODO*/ }, + onKeyboardAction = onKeyboardAction, ) { - BasicTextField( - modifier = Modifier.width(56.dp), - value = "新建分组", - onValueChange = {}, - textStyle = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) - ), - singleLine = true, - keyboardActions = KeyboardActions( - onDone = { - onKeyboardAction() - } - ) - ) + } } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt index df0c9b0..6b5cb3b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt @@ -13,6 +13,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.pager.ExperimentalPagerApi +import me.ash.reader.DataStoreKeys +import me.ash.reader.dataStore +import me.ash.reader.get +import me.ash.reader.spacerDollar import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.widget.Dialog import java.io.InputStream @@ -38,6 +42,10 @@ fun SubscribeDialog( LaunchedEffect(viewState.visible) { if (viewState.visible) { + val defaultGroupId = context.dataStore + .get(DataStoreKeys.CurrentAccountId)!! + .spacerDollar("0") + viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId)) viewModel.dispatch(SubscribeViewAction.Init) } else { viewModel.dispatch(SubscribeViewAction.Reset) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt index df02ad5..7b85cea 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt @@ -127,6 +127,14 @@ class SubscribeViewModel @Inject constructor( } viewModelScope.launch(Dispatchers.IO) { try { + if (rssRepository.get().isExist(_viewState.value.inputContent)) { + _viewState.update { + it.copy( + errorMessage = "已订阅", + ) + } + return@launch + } val feedWithArticle = rssHelper.searchFeed(_viewState.value.inputContent) _viewState.update { it.copy( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt new file mode 100644 index 0000000..199640a --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt @@ -0,0 +1,82 @@ +package me.ash.reader.ui.page.home.flow + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import me.ash.reader.DateTimeExt +import me.ash.reader.DateTimeExt.toString +import me.ash.reader.data.article.ArticleWithFeed + +@Composable +fun ArticleItem( + modifier: Modifier = Modifier, + articleWithFeed: ArticleWithFeed, + onClick: (ArticleWithFeed) -> Unit = {}, +) { + Column( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable { onClick(articleWithFeed) } + .padding(horizontal = 12.dp, vertical = 8.dp) + .alpha(if (articleWithFeed.article.isUnread) 1f else 0.5f), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(start = 30.dp), + text = articleWithFeed.feed.name, + color = MaterialTheme.colorScheme.tertiary, + style = MaterialTheme.typography.labelMedium, + ) + Text( + text = articleWithFeed.article.date.toString(DateTimeExt.HH_MM), + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.labelMedium, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)) + ) {} + Spacer(modifier = Modifier.width(10.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = articleWithFeed.article.title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = articleWithFeed.article.shortDescription, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt new file mode 100644 index 0000000..49e2d3b --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt @@ -0,0 +1,60 @@ +package me.ash.reader.ui.page.home.flow + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.paging.compose.LazyPagingItems +import kotlinx.coroutines.CoroutineScope +import me.ash.reader.DateTimeExt +import me.ash.reader.DateTimeExt.toString +import me.ash.reader.data.article.ArticleWithFeed +import me.ash.reader.ui.page.home.HomeViewAction +import me.ash.reader.ui.page.home.HomeViewModel +import me.ash.reader.ui.page.home.read.ReadViewAction +import me.ash.reader.ui.page.home.read.ReadViewModel + +@OptIn(ExperimentalFoundationApi::class) +fun LazyListScope.generateArticleList( + pagingItems: LazyPagingItems?, + readViewModel: ReadViewModel, + homeViewModel: HomeViewModel, + scope: CoroutineScope +) { + if (pagingItems == null) return + var lastItemDay: String? = null + for (itemIndex in 0 until pagingItems.itemCount) { + val currentItem = pagingItems.peek(itemIndex) ?: continue + val currentItemDay = currentItem.article.date + .toString(DateTimeExt.YYYY_MM_DD, true) + if (lastItemDay != currentItemDay) { + if (itemIndex != 0) { + item { Spacer(modifier = Modifier.height(40.dp)) } + } + stickyHeader { + StickyHeader(currentItemDay) + } + } + item { + ArticleItem( + articleWithFeed = currentItem, + ) { + readViewModel.dispatch(ReadViewAction.ScrollToItem(0)) + readViewModel.dispatch(ReadViewAction.InitData(it)) + if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) + else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) + readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 2, + ) + ) + } + } + + lastItemDay = currentItemDay + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt new file mode 100644 index 0000000..8b5c4ac --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -0,0 +1,121 @@ +package me.ash.reader.ui.page.home.flow + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.DoneAll +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.paging.compose.collectAsLazyPagingItems +import kotlinx.coroutines.flow.collect +import me.ash.reader.ui.extension.collectAsStateValue +import me.ash.reader.ui.page.home.HomeViewAction +import me.ash.reader.ui.page.home.HomeViewModel +import me.ash.reader.ui.page.home.read.ReadViewModel + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, +) +@Composable +fun FlowPage( + modifier: Modifier = Modifier, + navController: NavHostController, + viewModel: FlowViewModel = hiltViewModel(), + homeViewModel: HomeViewModel = hiltViewModel(), + readViewModel: ReadViewModel = hiltViewModel(), +) { + val scope = rememberCoroutineScope() + val viewState = viewModel.viewState.collectAsStateValue() + val filterState = homeViewModel.filterState.collectAsStateValue() + val pagingItems = viewState.pagingData?.collectAsLazyPagingItems() + + LaunchedEffect(homeViewModel.filterState) { + homeViewModel.filterState.collect { state -> + viewModel.dispatch( + FlowViewAction.FetchData(state) + ) + } + } + + Scaffold( + modifier = Modifier.background(MaterialTheme.colorScheme.surface), + topBar = { + SmallTopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = { + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 0, + ) + ) + }) { + Icon( + imageVector = Icons.Rounded.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, + actions = { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Rounded.DoneAll, + contentDescription = "Read All", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + ) + }, + content = { + LazyColumn( + state = viewState.listState, + ) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = if (true) 54.dp else 24.dp, + top = 48.dp, + end = 24.dp, + bottom = 24.dp + ), + text = when { + filterState.group != null -> filterState.group.name + filterState.feed != null -> filterState.feed.name + else -> filterState.filter.name + }, + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + generateArticleList(pagingItems, readViewModel, homeViewModel, scope) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt similarity index 63% rename from app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt rename to app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt index 30bc271..67e2d3a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt @@ -1,4 +1,4 @@ -package me.ash.reader.ui.page.home.article +package me.ash.reader.ui.page.home.flow import androidx.compose.foundation.lazy.LazyListState import androidx.lifecycle.ViewModel @@ -13,26 +13,22 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.ash.reader.data.article.ArticleWithFeed import me.ash.reader.data.repository.RssRepository +import me.ash.reader.ui.page.home.FilterState import javax.inject.Inject @HiltViewModel -class ArticleViewModel @Inject constructor( +class FlowViewModel @Inject constructor( private val rssRepository: RssRepository, ) : ViewModel() { private val _viewState = MutableStateFlow(ArticleViewState()) val viewState: StateFlow = _viewState.asStateFlow() - fun dispatch(action: ArticleViewAction) { + fun dispatch(action: FlowViewAction) { when (action) { - is ArticleViewAction.FetchData -> fetchData( - groupId = action.groupId, - feedId = action.feedId, - isStarred = action.isStarred, - isUnread = action.isUnread, - ) - is ArticleViewAction.ChangeRefreshing -> changeRefreshing(action.isRefreshing) - is ArticleViewAction.ScrollToItem -> scrollToItem(action.index) - is ArticleViewAction.PeekSyncWork -> peekSyncWork() + is FlowViewAction.FetchData -> fetchData(action.filterState) + is FlowViewAction.ChangeRefreshing -> changeRefreshing(action.isRefreshing) + is FlowViewAction.ScrollToItem -> scrollToItem(action.index) + is FlowViewAction.PeekSyncWork -> peekSyncWork() } } @@ -44,14 +40,9 @@ class ArticleViewModel @Inject constructor( } } - private fun fetchData( - groupId: String? = null, - feedId: String? = null, - isStarred: Boolean, - isUnread: Boolean, - ) { + private fun fetchData(filterState: FilterState) { viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().pullImportant(isStarred, true) + rssRepository.get().pullImportant(filterState.filter.isStarred(), true) .collect { importantList -> _viewState.update { it.copy( @@ -64,10 +55,10 @@ class ArticleViewModel @Inject constructor( it.copy( pagingData = Pager(PagingConfig(pageSize = 10)) { rssRepository.get().pullArticles( - groupId = groupId, - feedId = feedId, - isStarred = isStarred, - isUnread = isUnread, + groupId = filterState.group?.id, + feedId = filterState.feed?.id, + isStarred = filterState.filter.isStarred(), + isUnread = filterState.filter.isUnread(), ) }.flow.cachedIn(viewModelScope) ) @@ -95,21 +86,18 @@ data class ArticleViewState( val syncWorkInfo: String = "", ) -sealed class ArticleViewAction { +sealed class FlowViewAction { data class FetchData( - val groupId: String? = null, - val feedId: String? = null, - val isStarred: Boolean, - val isUnread: Boolean, - ) : ArticleViewAction() + val filterState: FilterState, + ) : FlowViewAction() data class ChangeRefreshing( val isRefreshing: Boolean - ) : ArticleViewAction() + ) : FlowViewAction() data class ScrollToItem( val index: Int - ) : ArticleViewAction() + ) : FlowViewAction() - object PeekSyncWork : ArticleViewAction() + object PeekSyncWork : FlowViewAction() } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleDateHeader.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt similarity index 56% rename from app/src/main/java/me/ash/reader/ui/page/home/article/ArticleDateHeader.kt rename to app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt index ef7c618..70fde2f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleDateHeader.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/StickyHeader.kt @@ -1,37 +1,29 @@ -package me.ash.reader.ui.page.home.article +package me.ash.reader.ui.page.home.flow import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp @Composable -fun ArticleDateHeader( - date: String, - isDisplayIcon: Boolean -) { +fun StickyHeader(currentItemDay: String) { Row( modifier = Modifier - .height(28.dp) .fillMaxWidth() .background(MaterialTheme.colorScheme.surface), verticalAlignment = Alignment.CenterVertically ) { Text( - text = date, - fontSize = 13.sp, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.padding(start = (if (isDisplayIcon) 52 else 20).dp), - fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(start = if (true) 54.dp else 24.dp), + text = currentItemDay, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, ) } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/widget/Banner.kt b/app/src/main/java/me/ash/reader/ui/widget/Banner.kt index ce5e291..5555fb4 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/Banner.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/Banner.kt @@ -22,8 +22,8 @@ fun Banner( title: String, desc: String? = null, icon: ImageVector? = null, + action: (@Composable () -> Unit)? = null, onClick: () -> Unit = {}, - action: (@Composable () -> Unit)? = null ) { Surface( modifier = modifier.fillMaxWidth(), diff --git a/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt b/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt index 613c7ab..b65400f 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt @@ -1,16 +1,20 @@ package me.ash.reader.ui.widget import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.RowScope +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.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.ChipDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FilterChip import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -20,20 +24,22 @@ import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterialApi::class) @Composable fun SelectionChip( + content: String, selected: Boolean, - onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = CircleShape, selectedIcon: @Composable () -> Unit = { Icon( - imageVector = Icons.Outlined.Check, + imageVector = Icons.Rounded.Check, contentDescription = "Check", - modifier = Modifier.size(20.dp) + modifier = Modifier + .padding(start = 8.dp) + .size(18.dp) ) }, - content: @Composable RowScope.() -> Unit + onClick: () -> Unit, ) { FilterChip( modifier = modifier, @@ -54,6 +60,83 @@ fun SelectionChip( selectedIcon = selectedIcon, shape = shape, onClick = onClick, - content = content, + content = { + Text( + modifier = modifier.padding( + start = if (selected) 0.dp else 8.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + ), + text = content, + style = MaterialTheme.typography.titleSmall, + ) + }, + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SelectionEditorChip( + content: String, + selected: Boolean, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = CircleShape, + selectedIcon: @Composable () -> Unit = { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = "Check", + modifier = Modifier + .padding(start = 8.dp) + .size(16.dp) + ) + }, + onKeyboardAction: () -> Unit = {}, + onClick: () -> Unit, +) { + FilterChip( + modifier = modifier, + colors = ChipDefaults.filterChipColors( + backgroundColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + leadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer, + disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer, + selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer, + selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer + ), + interactionSource = interactionSource, + enabled = enabled, + selected = selected, + selectedIcon = selectedIcon, + shape = shape, + onClick = onClick, + content = { + BasicTextField( + modifier = Modifier + .padding( + start = if (selected) 0.dp else 8.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + ) + .width(56.dp), + value = content, + onValueChange = {}, + textStyle = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onSurface + ), + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { + onKeyboardAction() + } + ) + ) + }, ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/widget/SubTitle.kt b/app/src/main/java/me/ash/reader/ui/widget/SubTitle.kt index f698d18..a78da2f 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/SubTitle.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/SubTitle.kt @@ -11,15 +11,15 @@ import androidx.compose.ui.unit.dp @Composable fun Subtitle( - text: String, modifier: Modifier = Modifier, + text: String, color: Color = MaterialTheme.colorScheme.primary, ) { Text( text = text, modifier = modifier .fillMaxWidth() - .padding(24.dp, 8.dp, 16.dp, 8.dp), + .padding(vertical = 8.dp), color = color, style = MaterialTheme.typography.labelLarge )