Refactor Material You design for FlowPage

This commit is contained in:
Ash 2022-03-19 21:10:22 +08:00
parent d288177feb
commit f4828ac01a
28 changed files with 627 additions and 642 deletions

View File

@ -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 <T : Any> rememberMutableStateListOf(vararg elements: T): SnapshotStateList<T> {
return rememberSaveable(
saver = listSaver(
save = { it.toList() },
restore = { it.toMutableStateList() }
)
) {
elements.toMutableList().toMutableStateList()
}
}
fun Int.spacerDollar(str: Any): String = "$this$$str"

View File

@ -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,

View File

@ -12,6 +12,15 @@ interface FeedDao {
)
suspend fun queryAll(accountId: Int): List<Feed>
@Query(
"""
SELECT * FROM feed
WHERE accountId = :accountId
and url = :url
"""
)
fun queryByLink(accountId: Int, url: String): List<Feed>
@Insert
suspend fun insert(feed: Feed): Long

View File

@ -22,6 +22,14 @@ interface GroupDao {
)
fun queryAllGroup(accountId: Int): Flow<MutableList<Group>>
@Query(
"""
SELECT * FROM `group`
WHERE accountId = :accountId
"""
)
suspend fun queryAll(accountId: Int): List<Group>
@Insert
suspend fun insert(group: Group): Long

View File

@ -108,7 +108,7 @@ abstract class AbstractRssRepository constructor(
isStarred: Boolean = false,
isUnread: Boolean = false,
): Flow<List<ImportantCount>> {
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()
}

View File

@ -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!!,
)
)
}
}
}
}

View File

@ -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,

View File

@ -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) {

View File

@ -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<Article>()
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("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
if (content != null) {
var iconLink = regex
.find(content)
?.groups?.get(3)
?.value
Log.i("rlog", "queryRssIcon: $iconLink")
if (iconLink != null) {
if (iconLink.startsWith("//")) {
iconLink = "http:$iconLink"
}
if (iconLink.startsWith("/")) {
val domainRegex =
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
iconLink =
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
}
saveRssIcon(feedDao, feed, iconLink)
} else {
try {
if (articleLink == null) return
val execute = OkHttpClient()
.newCall(Request.Builder().url(articleLink).build())
.execute()
val content = execute.body?.string()
val regex =
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
if (content != null) {
var iconLink = regex
.find(content)
?.groups?.get(3)
?.value
Log.i("rlog", "queryRssIcon: $iconLink")
if (iconLink != null) {
if (iconLink.startsWith("//")) {
iconLink = "http:$iconLink"
}
if (iconLink.startsWith("/")) {
val domainRegex =
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
iconLink =
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
}
saveRssIcon(feedDao, feed, iconLink)
} else {
// saveRssIcon(feedDao, feed, "")
}
} else {
}
} else {
// saveRssIcon(feedDao, feed, "")
}
} catch (e: Exception) {
Log.e("RLog", "queryRssIcon: ${e.message}")
}
}

View File

@ -19,8 +19,8 @@ import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Symbol
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.page.home.article.ArticlePage
import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
@ -98,35 +98,10 @@ fun HomePage(
state = viewState.pagerState,
composableList = listOf(
{
FeedsPage(
navController = navController,
)
FeedsPage(navController = navController)
},
{
ArticlePage(
navController = navController,
BackOnClick = {
viewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 0,
)
)
},
articleOnClick = {
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)
viewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 2,
)
)
},
)
FlowPage(navController = navController)
},
{
ReadPage(

View File

@ -1,143 +0,0 @@
package me.ash.reader.ui.page.home.article
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick
@Composable
fun ArticleItem(
modifier: Modifier = Modifier,
articleWithFeed: ArticleWithFeed? = null,
isStarredFilter: Boolean,
index: Int,
articleOnClick: (ArticleWithFeed) -> 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
)
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}
}

View File

@ -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
)
}
},
)
}

View File

@ -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,

View File

@ -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))
}
}
}

View File

@ -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(

View File

@ -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<Feed>,
@ -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,

View File

@ -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()
}
)
)
}
}
}

View File

@ -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)

View File

@ -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(

View File

@ -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,
)
}
}
}
}

View File

@ -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<ArticleWithFeed>?,
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
}
}

View File

@ -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)
}
}
)
}

View File

@ -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<ArticleViewState> = _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()
}

View File

@ -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,
)
}
}

View File

@ -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(),

View File

@ -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()
}
)
)
},
)
}

View File

@ -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
)