Optimize the feeds page

This commit is contained in:
Ash 2022-05-31 02:21:28 +08:00
parent 5867186751
commit 433ff6e6f2
16 changed files with 485 additions and 386 deletions

View File

@ -10,12 +10,12 @@ import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.*
import me.ash.reader.data.model.ImportantCount
import me.ash.reader.ui.ext.currentAccountId
import java.util.*
@ -99,7 +99,7 @@ abstract class AbstractRssRepository constructor(
fun pullImportant(
isStarred: Boolean = false,
isUnread: Boolean = false,
): Flow<List<ImportantCount>> {
): Flow<Map<String, Int>> {
val accountId = context.currentAccountId
Log.i(
"RLog",
@ -111,6 +111,12 @@ abstract class AbstractRssRepository constructor(
isUnread -> articleDao
.queryImportantCountWhenIsUnread(accountId, isUnread)
else -> articleDao.queryImportantCountWhenIsAll(accountId)
}.mapLatest {
mapOf(
*(it.map {
it.feedId to it.important
}.toTypedArray())
)
}.flowOn(dispatcherIO)
}

View File

@ -4,6 +4,7 @@ import android.os.Build
import android.view.SoundEffectConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@ -20,7 +21,6 @@ import me.ash.reader.ui.theme.palette.onDark
@Composable
fun FilterBar(
modifier: Modifier = Modifier,
filter: Filter,
filterBarStyle: Int,
filterBarFilled: Boolean,
@ -33,7 +33,8 @@ fun FilterBar(
NavigationBar(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(filterBarTonalElevation)),
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(filterBarTonalElevation))
.navigationBarsPadding(),
tonalElevation = filterBarTonalElevation,
) {
Spacer(modifier = Modifier.width(filterBarPadding))

View File

@ -31,14 +31,14 @@ fun RYScaffold(
color = containerColor
)
)
.statusBarsPadding()
.run {
if (bottomBar != null || floatingActionButton != null) {
navigationBarsPadding()
} else {
this
}
},
.statusBarsPadding(),
// .run {
// if (bottomBar != null || floatingActionButton != null) {
// navigationBarsPadding()
// } else {
// this
// }
// },
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
containerTonalElevation,
color = containerColor

View File

@ -67,7 +67,7 @@ fun Context.share(content: String) {
}, getString(R.string.share)))
}
fun Context.openURL(url: String? = null) {
fun Context.openURL(url: String?) {
url?.takeIf { it.trim().isNotEmpty() }
?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
}

View File

@ -1,95 +1,99 @@
package me.ash.reader.ui.page.home.feeds
import RYExtensibleVisibility
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Badge
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.entity.Feed
import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionViewModel
import kotlin.math.ln
import me.ash.reader.ui.theme.ShapeBottom32
@OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class,
androidx.compose.material.ExperimentalMaterialApi::class,
)
@Composable
fun FeedItem(
feed: Feed,
alpha: Float = 1f,
badgeAlpha: Float = 1f,
isEnded: Boolean = false,
isExpanded: () -> Boolean,
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
tonalElevation: Dp,
onClick: () -> Unit = {},
) {
val view = LocalView.current
val scope = rememberCoroutineScope()
val tonalElevationAlpha by remember {
derivedStateOf {
(ln(tonalElevation.value + 1.4f) + 2f) / 100f
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.clip(RoundedCornerShape(32.dp))
.combinedClickable(
onClick = {
onClick()
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.showDrawer(scope, feed.id)
}
)
.padding(vertical = 14.dp),
) {
RYExtensibleVisibility(visible = isExpanded()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 14.dp, end = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(modifier = Modifier.weight(1f)) {
FeedIcon(feed.name)
Text(
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
text = feed.name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
.padding(horizontal = 16.dp)
.background(
color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha),
shape = if (isEnded) ShapeBottom32 else RectangleShape,
)
}
if ((feed.important ?: 0) != 0) {
Badge(
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = tonalElevationAlpha
),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
.combinedClickable(
onClick = {
onClick()
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.showDrawer(scope, feed.id)
}
)
.padding(horizontal = 14.dp)
.padding(top = 14.dp, bottom = if (isEnded) 22.dp else 14.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 14.dp, end = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(modifier = Modifier.weight(1f)) {
FeedIcon(feed.name)
Text(
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
text = feed.name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if ((feed.important ?: 0) != 0) {
Badge(
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
alpha = badgeAlpha
),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
},
)
}
}
}
}

View File

@ -31,6 +31,7 @@ import me.ash.reader.data.preference.*
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.base.*
import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity
import me.ash.reader.ui.ext.getCurrentVersion
@ -41,9 +42,9 @@ import me.ash.reader.ui.page.home.feeds.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionDrawer
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import kotlin.math.ln
@OptIn(
com.google.accompanist.pager.ExperimentalPagerApi::class,
androidx.compose.foundation.ExperimentalFoundationApi::class
)
@Composable
@ -96,6 +97,24 @@ fun FeedsPage(
}
}
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
val groupIndicatorAlpha by remember {
derivedStateOf {
groupListTonalElevation.value.dp.alphaLN(
weight = 1.4f
)
}
}
val groupsVisible = remember(feedsUiState.groupWithFeedList) {
mutableStateMapOf(
*(feedsUiState.groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
it.group.id to groupListExpand.value
}.toTypedArray())
)
}
LaunchedEffect(Unit) {
feedsViewModel.fetchAccount()
}
@ -161,7 +180,7 @@ fun FeedsPage(
item {
Banner(
title = filterUiState.filter.getName(),
desc = feedsUiState.importantCount.ifEmpty { stringResource(R.string.loading) },
desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) },
icon = filterUiState.filter.iconOutline,
action = {
Icon(
@ -189,14 +208,21 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(8.dp))
}
itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
// Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column {
GroupItem(
isExpanded = groupListExpand.value,
tonalElevation = groupListTonalElevation.value.dp,
group = groupWithFeed.group,
feeds = groupWithFeed.feeds,
groupOnClick = {
when (groupWithFeed) {
is GroupFeedsView.Group -> {
if (index != 0) {
Spacer(modifier = Modifier.height(16.dp))
}
GroupItem(
isExpanded = { groupsVisible[groupWithFeed.group.id] ?: false },
group = groupWithFeed.group,
alpha = groupAlpha,
indicatorAlpha = groupIndicatorAlpha,
onExpanded = {
groupsVisible[groupWithFeed.group.id] =
!(groupsVisible[groupWithFeed.group.id] ?: false)
}
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
@ -205,23 +231,27 @@ fun FeedsPage(
feed = null,
)
)
},
feedOnClick = { feed ->
}
}
is GroupFeedsView.Feed -> {
FeedItem(
feed = groupWithFeed.feed,
alpha = groupAlpha,
badgeAlpha = feedBadgeAlpha,
isEnded = index != feedsUiState.groupWithFeedList.lastIndex && feedsUiState.groupWithFeedList[index + 1] is GroupFeedsView.Group,
isExpanded = { groupsVisible[groupWithFeed.feed.groupId] ?: false },
) {
filterChange(
navController = navController,
homeViewModel = homeViewModel,
filterState = filterUiState.copy(
group = null,
feed = feed,
feed = groupWithFeed.feed,
)
)
}
)
if (index != feedsUiState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp))
}
}
// }
}
item {
Spacer(modifier = Modifier.height(128.dp))

View File

@ -2,6 +2,7 @@ package me.ash.reader.ui.page.home.feeds
import android.util.Log
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -10,7 +11,6 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.GroupWithFeed
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.repository.AccountRepository
@ -67,42 +67,20 @@ class FeedsViewModel @Inject constructor(
combine(
rssRepository.get().pullFeeds(),
rssRepository.get().pullImportant(isStarred, isUnread),
) { groupWithFeedList, importantList ->
val groupImportantMap = mutableMapOf<String, Int>()
val feedImportantMap = mutableMapOf<String, Int>()
importantList.groupBy { it.groupId }.forEach { (i, list) ->
var groupImportantSum = 0
list.forEach {
feedImportantMap[it.feedId] = it.important
groupImportantSum += it.important
}
groupImportantMap[i] = groupImportantSum
}
val groupsIt = groupWithFeedList.iterator()
while (groupsIt.hasNext()) {
val groupWithFeed = groupsIt.next()
val groupImportant = groupImportantMap[groupWithFeed.group.id]
if (groupImportant == null && (isStarred || isUnread)) {
groupsIt.remove()
} else {
groupWithFeed.group.important = groupImportant
val feedsIt = groupWithFeed.feeds.iterator()
while (feedsIt.hasNext()) {
val feed = feedsIt.next()
val feedImportant = feedImportantMap[feed.id]
if (feedImportant == null && (isStarred || isUnread)) {
feedsIt.remove()
} else {
feed.important = feedImportant
}
}
) { groupWithFeedList, importantMap ->
groupWithFeedList.fastForEach {
var groupImportant = 0
it.feeds.fastForEach {
it.important = importantMap[it.id]
groupImportant += it.important ?: 0
}
it.group.important = groupImportant
}
groupWithFeedList
}.onEach { groupWithFeedList ->
}.mapLatest { groupWithFeedList ->
_feedsUiState.update {
it.copy(
importantCount = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
importantSum = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
when {
isStarred -> stringsRepository.getQuantityString(
R.plurals.starred_desc,
@ -121,8 +99,15 @@ class FeedsViewModel @Inject constructor(
)
}
},
groupWithFeedList = groupWithFeedList,
feedsVisible = List(groupWithFeedList.size, init = { true })
groupWithFeedList = groupWithFeedList.map {
mutableListOf<GroupFeedsView>(GroupFeedsView.Group(it.group)).apply {
addAll(
it.feeds.map {
GroupFeedsView.Feed(it)
}
)
}
}.flatten(),
)
}
}.catch {
@ -133,9 +118,13 @@ class FeedsViewModel @Inject constructor(
data class FeedsUiState(
val account: Account? = null,
val importantCount: String = "",
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
val feedsVisible: List<Boolean> = emptyList(),
val importantSum: String = "",
val groupWithFeedList: List<GroupFeedsView> = emptyList(),
val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true,
)
sealed class GroupFeedsView {
class Group(val group: me.ash.reader.data.entity.Group) : GroupFeedsView()
class Feed(val feed: me.ash.reader.data.entity.Feed) : GroupFeedsView()
}

View File

@ -1,58 +1,56 @@
package me.ash.reader.ui.page.home.feeds
import RYExtensibleVisibility
import android.view.HapticFeedbackConstants
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.page.home.feeds.drawer.group.GroupOptionViewModel
import me.ash.reader.ui.theme.Shape32
import me.ash.reader.ui.theme.ShapeTop32
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
fun GroupItem(
modifier: Modifier = Modifier,
tonalElevation: Dp,
group: Group,
feeds: List<Feed>,
isExpanded: Boolean = true,
alpha: Float = 1f,
indicatorAlpha: Float = 1f,
isExpanded: () -> Boolean,
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
onExpanded: () -> Unit = {},
groupOnClick: () -> Unit = {},
feedOnClick: (feed: Feed) -> Unit = {},
) {
val view = LocalView.current
val scope = rememberCoroutineScope()
var expanded by remember { mutableStateOf(isExpanded) }
Column(
modifier = Modifier
.animateContentSize()
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp))
.clip(if (isExpanded()) ShapeTop32 else Shape32)
.background(
MaterialTheme.colorScheme.secondary.copy(alpha = tonalElevation.alphaLN(weight = 1.2f))
MaterialTheme.colorScheme.secondary.copy(alpha = alpha)
)
.combinedClickable(
onClick = {
@ -66,7 +64,7 @@ fun GroupItem(
.padding(top = 22.dp)
) {
Row(
modifier = modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -86,38 +84,21 @@ fun GroupItem(
.size(24.dp)
.clip(CircleShape)
.background(
MaterialTheme.colorScheme.surfaceTint.copy(
alpha = tonalElevation.alphaLN(weight = 1.4f)
)
MaterialTheme.colorScheme.surfaceTint.copy(alpha = indicatorAlpha)
)
.clickable {
expanded = !expanded
onExpanded()
},
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = stringResource(if (expanded) R.string.expand_less else R.string.expand_more),
imageVector = if (isExpanded()) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = stringResource(if (isExpanded()) R.string.expand_less else R.string.expand_more),
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
Spacer(modifier = Modifier.height(22.dp))
RYExtensibleVisibility(visible = expanded) {
Column {
feeds.forEach { feed ->
FeedItem(
feed = feed,
tonalElevation = tonalElevation,
) {
feedOnClick(feed)
}
}
if (feeds.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.feeds.drawer.feed
import android.view.HapticFeedbackConstants
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
@ -13,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -23,6 +25,7 @@ import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.component.base.BottomDrawer
import me.ash.reader.ui.component.base.TextFieldDialog
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.openURL
import me.ash.reader.ui.ext.roundClick
import me.ash.reader.ui.ext.showToast
import me.ash.reader.ui.page.home.feeds.subscribe.ResultView
@ -34,6 +37,7 @@ fun FeedOptionDrawer(
content: @Composable () -> Unit = {},
) {
val context = LocalContext.current
val view = LocalView.current
val scope = rememberCoroutineScope()
val feedOptionUiState = feedOptionViewModel.feedOptionUiState.collectAsStateValue()
val feed = feedOptionUiState.feed
@ -77,7 +81,8 @@ fun FeedOptionDrawer(
ResultView(
link = feed?.url ?: stringResource(R.string.unknown),
groups = feedOptionUiState.groups,
selectedAllowNotificationPreset = feedOptionUiState.feed?.isNotification ?: false,
selectedAllowNotificationPreset = feedOptionUiState.feed?.isNotification
?: false,
selectedParseFullContentPreset = feedOptionUiState.feed?.isFullContent ?: false,
isMoveToGroup = true,
showUnsubscribe = true,
@ -101,6 +106,10 @@ fun FeedOptionDrawer(
feedOptionViewModel.showNewGroupDialog()
},
onFeedUrlClick = {
context.openURL(feed?.url)
},
onFeedUrlLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.showFeedUrlDialog()
}
)

View File

@ -1,15 +1,11 @@
package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Article
@ -32,7 +28,6 @@ import me.ash.reader.R
import me.ash.reader.data.entity.Group
import me.ash.reader.ui.component.base.RYSelectionChip
import me.ash.reader.ui.component.base.Subtitle
import me.ash.reader.ui.ext.roundClick
import me.ash.reader.ui.theme.palette.alwaysLight
@Composable
@ -51,7 +46,8 @@ fun ResultView(
unsubscribeOnClick: () -> Unit = {},
onGroupClick: (groupId: String) -> Unit = {},
onAddNewGroup: () -> Unit = {},
onFeedUrlClick: () -> Unit = {}
onFeedUrlClick: () -> Unit = {},
onFeedUrlLongClick: () -> Unit = {},
) {
LaunchedEffect(Unit) {
if (groups.isNotEmpty() && selectedGroupId.isEmpty()) onGroupClick(groups.first().id)
@ -60,7 +56,11 @@ fun ResultView(
Column(
modifier = modifier.verticalScroll(rememberScrollState())
) {
EditableUrl(text = link, onFeedUrlClick)
EditableUrl(
text = link,
onClick = onFeedUrlClick,
onLongClick = onFeedUrlLongClick,
)
Spacer(modifier = Modifier.height(26.dp))
Preset(
@ -85,27 +85,30 @@ fun ResultView(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun EditableUrl(
text: String,
onClick: () -> Unit
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
SelectionContainer {
Text(
modifier = Modifier.roundClick {
onClick()
},
text = text,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Text(
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
),
text = text,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

View File

@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -25,14 +24,13 @@ import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.FeedIcon
import me.ash.reader.ui.component.base.RYAsyncImage
import me.ash.reader.ui.component.base.SIZE_1000
import me.ash.reader.ui.theme.SHAPE_20
import me.ash.reader.ui.theme.Shape20
@Composable
fun ArticleItem(
articleWithFeed: ArticleWithFeed,
onClick: (ArticleWithFeed) -> Unit = {},
) {
val context = LocalContext.current
val articleListFeedIcon = LocalFlowArticleListFeedIcon.current
val articleListFeedName = LocalFlowArticleListFeedName.current
val articleListImage = LocalFlowArticleListImage.current
@ -42,12 +40,12 @@ fun ArticleItem(
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
.clip(SHAPE_20)
.clip(Shape20)
.clickable { onClick(articleWithFeed) }
.padding(horizontal = 12.dp, vertical = 12.dp)
.alpha(if (articleWithFeed.article.isStarred || articleWithFeed.article.isUnread) 1f else 0.5f),
) {
// Upper
// Top
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@ -99,7 +97,7 @@ fun ArticleItem(
}
}
// Lower
// Bottom
Row(
modifier = Modifier.fillMaxWidth(),
) {
@ -142,7 +140,7 @@ fun ArticleItem(
modifier = Modifier
.padding(start = 10.dp)
.size(80.dp)
.clip(SHAPE_20),
.clip(Shape20),
data = articleWithFeed.article.img,
scale = Scale.FILL,
precision = Precision.INEXACT,

View File

@ -0,0 +1,143 @@
package me.ash.reader.ui.page.settings.color.feeds
import androidx.compose.animation.animateContentSize
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.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.FeedsGroupListExpandPreference
import me.ash.reader.data.preference.FeedsGroupListTonalElevationPreference
import me.ash.reader.data.preference.FeedsTopBarTonalElevationPreference
import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.ext.alphaLN
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.home.feeds.FeedItem
import me.ash.reader.ui.page.home.feeds.GroupItem
import me.ash.reader.ui.theme.palette.onDark
import kotlin.math.ln
@Composable
fun FeedsPagePreview(
topBarTonalElevation: FeedsTopBarTonalElevationPreference,
groupListExpand: FeedsGroupListExpandPreference,
groupListTonalElevation: FeedsGroupListTonalElevationPreference,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
) {
var filter by remember { mutableStateOf(Filter.Unread) }
val feedBadgeAlpha by remember { derivedStateOf { (ln(groupListTonalElevation.value + 1.4f) + 2f) / 100f } }
val groupAlpha by remember { derivedStateOf { groupListTonalElevation.value.dp.alphaLN(weight = 1.2f) } }
val groupIndicatorAlpha by remember {
derivedStateOf {
groupListTonalElevation.value.dp.alphaLN(
weight = 1.4f
)
}
}
Column(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
groupListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
},
actions = {
FeedbackIconButton(
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
)
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
)
}
)
Spacer(modifier = Modifier.height(12.dp))
GroupItem(
isExpanded = { groupListExpand.value },
group = generateGroupPreview(),
alpha = groupAlpha,
indicatorAlpha = groupIndicatorAlpha,
)
FeedItem(
feed = generateFeedPreview(),
alpha = groupAlpha,
badgeAlpha = feedBadgeAlpha,
isEnded = true,
isExpanded = { true },
)
Spacer(modifier = Modifier.height(12.dp))
FilterBar(
filter = filter,
filterBarStyle = filterBarStyle,
filterBarFilled = filterBarFilled,
filterBarPadding = filterBarPadding,
filterBarTonalElevation = filterBarTonalElevation,
) {
filter = it
}
}
}
@Stable
@Composable
fun generateFeedPreview(): Feed =
Feed(
id = "",
name = stringResource(R.string.preview_feed_name),
icon = "",
accountId = 0,
groupId = "",
url = "",
).apply {
important = 100
}
@Stable
@Composable
fun generateGroupPreview(): Group =
Group(
id = "",
name = stringResource(R.string.defaults),
accountId = 0,
)

View File

@ -1,38 +1,25 @@
package me.ash.reader.ui.page.settings.color.feeds
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.base.*
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.home.feeds.GroupItem
import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.theme.palette.onDark
import me.ash.reader.ui.theme.palette.onLight
@Composable
@ -270,88 +257,3 @@ fun FeedsPageStylePage(
groupListTonalElevationDialogVisible = false
}
}
@Composable
fun FeedsPagePreview(
topBarTonalElevation: FeedsTopBarTonalElevationPreference,
groupListExpand: FeedsGroupListExpandPreference,
groupListTonalElevation: FeedsGroupListTonalElevationPreference,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
) {
var filter by remember { mutableStateOf(Filter.Unread) }
Column(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
groupListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {}
},
actions = {
FeedbackIconButton(
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
) {}
FeedbackIconButton(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
) {}
}
)
Spacer(modifier = Modifier.height(12.dp))
GroupItem(
isExpanded = groupListExpand.value,
tonalElevation = groupListTonalElevation.value.dp,
group = Group(
id = "",
name = stringResource(R.string.defaults),
accountId = 0,
),
feeds = listOf(
Feed(
id = "",
name = stringResource(R.string.preview_feed_name),
icon = "",
accountId = 0,
groupId = "",
url = "",
).apply {
important = 100
}
),
)
Spacer(modifier = Modifier.height(12.dp))
FilterBar(
modifier = Modifier.padding(horizontal = 12.dp),
filter = filter,
filterBarStyle = filterBarStyle,
filterBarFilled = filterBarFilled,
filterBarPadding = filterBarPadding,
filterBarTonalElevation = filterBarTonalElevation,
) {
filter = it
}
}
}

View File

@ -0,0 +1,124 @@
package me.ash.reader.ui.page.settings.color.flow
import androidx.compose.animation.animateContentSize
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.shape.RoundedCornerShape
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.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.FlowArticleListTonalElevationPreference
import me.ash.reader.data.preference.FlowTopBarTonalElevationPreference
import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.home.flow.ArticleItem
import me.ash.reader.ui.theme.palette.onDark
import java.util.*
@Composable
fun FlowPagePreview(
topBarTonalElevation: FlowTopBarTonalElevationPreference,
articleListTonalElevation: FlowArticleListTonalElevationPreference,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
) {
var filter by remember { mutableStateOf(Filter.Unread) }
Column(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
articleListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {}
},
actions = {
FeedbackIconButton(
imageVector = Icons.Rounded.DoneAll,
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
) {}
FeedbackIconButton(
imageVector = Icons.Rounded.Search,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface,
) {}
}
)
Spacer(modifier = Modifier.height(12.dp))
ArticleItem(
articleWithFeed = generateArticleWithFeedPreview(),
)
Spacer(modifier = Modifier.height(12.dp))
FilterBar(
filter = filter,
filterBarStyle = filterBarStyle,
filterBarFilled = filterBarFilled,
filterBarPadding = filterBarPadding,
filterBarTonalElevation = filterBarTonalElevation,
) {
filter = it
}
}
}
@Stable
@Composable
fun generateArticleWithFeedPreview(): ArticleWithFeed =
ArticleWithFeed(
Article(
id = "",
title = stringResource(R.string.preview_article_title),
shortDescription = stringResource(R.string.preview_article_desc),
rawDescription = stringResource(R.string.preview_article_desc),
link = "",
feedId = "",
accountId = 0,
date = Date(1654053729L),
isStarred = true,
img = "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60"
),
feed = Feed(
id = "",
name = stringResource(R.string.preview_feed_name),
icon = "",
accountId = 0,
groupId = "",
url = "",
),
)

View File

@ -1,6 +1,5 @@
package me.ash.reader.ui.page.settings.color.flow
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -8,34 +7,20 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
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.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.model.Filter
import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.FilterBar
import me.ash.reader.ui.component.base.*
import me.ash.reader.ui.ext.surfaceColorAtElevation
import me.ash.reader.ui.page.home.flow.ArticleItem
import me.ash.reader.ui.page.settings.SettingItem
import me.ash.reader.ui.theme.palette.onDark
import me.ash.reader.ui.theme.palette.onLight
import java.util.*
@Composable
fun FlowPageStylePage(
@ -332,90 +317,3 @@ fun FlowPageStylePage(
articleListTonalElevationDialogVisible = false
}
}
@Composable
fun FlowPagePreview(
topBarTonalElevation: FlowTopBarTonalElevationPreference,
articleListTonalElevation: FlowArticleListTonalElevationPreference,
filterBarStyle: Int,
filterBarFilled: Boolean,
filterBarPadding: Dp,
filterBarTonalElevation: Dp,
) {
var filter by remember { mutableStateOf(Filter.Unread) }
Column(
modifier = Modifier
.animateContentSize()
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
articleListTonalElevation.value.dp
) onDark MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp)
)
) {
SmallTopAppBar(
title = {},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
topBarTonalElevation.value.dp
),
),
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
) {}
},
actions = {
FeedbackIconButton(
imageVector = Icons.Rounded.DoneAll,
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
) {}
FeedbackIconButton(
imageVector = Icons.Rounded.Search,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface,
) {}
}
)
Spacer(modifier = Modifier.height(12.dp))
ArticleItem(
articleWithFeed = ArticleWithFeed(
Article(
id = "",
title = stringResource(R.string.preview_article_title),
shortDescription = stringResource(R.string.preview_article_desc),
rawDescription = stringResource(R.string.preview_article_desc),
link = "",
feedId = "",
accountId = 0,
date = Date(),
isStarred = true,
img = "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60"
),
feed = Feed(
id = "",
name = stringResource(R.string.preview_feed_name),
icon = "",
accountId = 0,
groupId = "",
url = "",
),
)
)
Spacer(modifier = Modifier.height(12.dp))
FilterBar(
modifier = Modifier.padding(horizontal = 12.dp),
filter = filter,
filterBarStyle = filterBarStyle,
filterBarFilled = filterBarFilled,
filterBarPadding = filterBarPadding,
filterBarTonalElevation = filterBarTonalElevation,
) {
filter = it
}
}
}

View File

@ -2,6 +2,7 @@ package me.ash.reader.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
@ -12,4 +13,14 @@ val Shapes = Shapes(
extraLarge = RoundedCornerShape(28.0.dp)
)
val SHAPE_20 = RoundedCornerShape(20.0.dp)
@Stable
val Shape20 = RoundedCornerShape(20.0.dp)
@Stable
val Shape32 = RoundedCornerShape(32.0.dp)
@Stable
val ShapeTop32 = RoundedCornerShape(32.0.dp, 32.0.dp, 0.0.dp, 0.0.dp)
@Stable
val ShapeBottom32 = RoundedCornerShape(0.0.dp, 0.0.dp, 32.0.dp, 32.0.dp)