Refactor Material You design for FeedsPage

This commit is contained in:
Ash 2022-03-19 18:27:12 +08:00
parent f99fc8698a
commit d288177feb
32 changed files with 628 additions and 691 deletions

View File

@ -25,7 +25,7 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
// shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }

View File

@ -1,29 +1,39 @@
package me.ash.reader.data.constant package me.ash.reader.data.constant
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject
import androidx.compose.ui.graphics.vector.ImageVector
class Filter( class Filter(
var index: Int, var index: Int,
var title: String, var title: String,
var description: String, var description: String,
var important: Int, var important: Int,
var icon: ImageVector,
) { ) {
companion object { companion object {
val Starred = Filter( val Starred = Filter(
index = 0, index = 0,
title = "Starred", title = "Starred",
description = " Starred Items", description = " Starred Items",
important = 13 important = 13,
icon = Icons.Rounded.StarOutline,
) )
val Unread = Filter( val Unread = Filter(
index = 1, index = 1,
title = "Unread", title = "Unread",
description = " Unread Items", description = " Unread Items",
important = 666 important = 666,
icon = Icons.Outlined.FiberManualRecord,
) )
val All = Filter( val All = Filter(
index = 2, index = 2,
title = "All", title = "All",
description = " Unread Items", description = " Unread Items",
important = 666 important = 666,
icon = Icons.Rounded.Subject,
) )
} }
} }

View File

@ -26,7 +26,7 @@ class AccountRepository @Inject constructor(
suspend fun addDefaultAccount(): Account { suspend fun addDefaultAccount(): Account {
return Account( return Account(
name = "Reader You", name = "Read You",
type = Account.Type.LOCAL, type = Account.Type.LOCAL,
).apply { ).apply {
id = accountDao.insert(this).toInt() id = accountDao.insert(this).toInt()

View File

@ -20,7 +20,7 @@ import me.ash.reader.data.constant.Symbol
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.page.home.article.ArticlePage import me.ash.reader.ui.page.home.article.ArticlePage
import me.ash.reader.ui.page.home.feed.FeedPage import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.read.ReadPage 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.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel import me.ash.reader.ui.page.home.read.ReadViewModel
@ -98,25 +98,8 @@ fun HomePage(
state = viewState.pagerState, state = viewState.pagerState,
composableList = listOf( composableList = listOf(
{ {
FeedPage( FeedsPage(
navController = navController, navController = navController,
filter = filterState.filter,
groupAndFeedOnClick = { currentGroup, currentFeed ->
viewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
group = currentGroup,
feed = currentFeed,
)
)
)
viewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
)
)
},
) )
}, },
{ {

View File

@ -1,231 +0,0 @@
package me.ash.reader.ui.page.home.feed
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.Image
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalView
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.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.R
import me.ash.reader.ui.widget.AnimatedText
import me.ash.reader.ui.widget.Menu
@Composable
fun ButtonBar(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
buttonBarType: ButtonBarType,
) {
var expanded by remember { mutableStateOf(false) }
var offset by remember { mutableStateOf(DpOffset.Zero) }
val interactionSource = remember { MutableInteractionSource() }
val view = LocalView.current
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.height(50.dp)
.indication(interactionSource, LocalIndication.current)
.padding(horizontal = 20.dp)
.clip(RoundedCornerShape(8.dp))
.pointerInput(Unit) {
detectTapGestures(
onPress = { offset ->
val press = PressInteraction.Press(offset)
interactionSource.emit(press)
tryAwaitRelease()
interactionSource.emit(PressInteraction.Release(press))
},
onTap = {
onClick()
},
onLongPress = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
offset = DpOffset(it.x.toDp(), it.y.toDp())
expanded = true
},
)
},
) {
when (buttonBarType) {
is ButtonBarType.FilterBar -> FilterBar(buttonBarType)
is ButtonBarType.AllBar -> AllBar(buttonBarType)
is ButtonBarType.GroupBar -> GroupBar(buttonBarType)
is ButtonBarType.FeedBar -> FeedBar(buttonBarType)
}
}
Menu(
offset = offset,
expanded = expanded,
dismissFunction = { expanded = false },
)
}
@Composable
fun FilterBar(
buttonBarType: ButtonBarType.FilterBar,
) {
AnimatedText(
text = buttonBarType.title,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
)
AnimatedText(
text = buttonBarType.important.toString(),
fontSize = 18.sp,
color = MaterialTheme.colorScheme.outline,
)
}
@Composable
fun AllBar(
buttonBarType: ButtonBarType.AllBar,
) {
AnimatedText(
text = buttonBarType.title,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
)
Icon(
imageVector = buttonBarType.icon,
contentDescription = "Expand More",
tint = MaterialTheme.colorScheme.outline,
)
}
@Composable
fun GroupBar(
buttonBarType: ButtonBarType.GroupBar,
) {
Row {
Icon(
imageVector = buttonBarType.icon,
contentDescription = "icon",
modifier = Modifier
.padding(end = 4.dp)
.clip(CircleShape)
.clickable(onClick = buttonBarType.iconOnClick),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
Text(
modifier = Modifier
.weight(1f)
.padding(end = 20.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = buttonBarType.title,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
AnimatedText(
text = buttonBarType.important.toString(),
fontSize = 18.sp,
color = MaterialTheme.colorScheme.outline,
)
}
@Composable
fun FeedBar(
buttonBarType: ButtonBarType.FeedBar,
) {
Row {
Surface(
modifier = Modifier
.padding(start = 28.dp, end = 4.dp)
.size(24.dp),
//.background(MaterialTheme.colorScheme.inversePrimary),
color = if (buttonBarType.icon == null) {
MaterialTheme.colorScheme.inversePrimary
} else {
Color.Unspecified
}
) {
if (buttonBarType.icon == null) {
Icon(
painter = painterResource(id = R.drawable.default_folder),
contentDescription = "icon",
modifier = Modifier.fillMaxSize(),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
} else {
Image(
painter = buttonBarType.icon,
contentDescription = "icon",
modifier = Modifier.fillMaxSize(),
)
}
}
Text(
modifier = Modifier
.padding(end = 20.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = buttonBarType.title,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
AnimatedText(
text = buttonBarType.important.toString(),
fontSize = 18.sp,
color = MaterialTheme.colorScheme.outline,
)
}
sealed class ButtonBarType {
data class FilterBar(
val modifier: Modifier = Modifier,
val title: String = "",
val important: Int = 0,
) : ButtonBarType()
data class AllBar(
val title: String = "",
val icon: ImageVector,
) : ButtonBarType()
data class GroupBar(
val title: String = "",
val important: Int = 0,
val icon: ImageVector,
val iconOnClick: () -> Unit = {},
) : ButtonBarType()
data class FeedBar(
val title: String = "",
val important: Int = 0,
val icon: Painter? = null,
) : ButtonBarType()
}

View File

@ -1,49 +0,0 @@
package me.ash.reader.ui.page.home.feed
import android.graphics.BitmapFactory
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import me.ash.reader.data.feed.Feed
@Composable
fun ColumnScope.FeedList(
visible: Boolean,
feeds: List<Feed>,
onClick: (currentFeed: Feed?) -> Unit = {},
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column(modifier = Modifier.animateContentSize()) {
feeds.forEach { feed ->
ButtonBar(
buttonBarType = ButtonBarType.FeedBar(
title = feed.name,
important = feed.important ?: 0,
icon = if (feed.icon == null) {
null
} else {
BitmapPainter(
BitmapFactory.decodeByteArray(
feed.icon,
0,
feed.icon!!.size
).asImageBitmap()
)
},
),
onClick = {
onClick(feed)
},
)
}
}
}
}

View File

@ -1,139 +0,0 @@
package me.ash.reader.ui.page.home.feed
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
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.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
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.feed.subscribe.SubscribeDialog
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeViewModel
import me.ash.reader.ui.widget.TopTitleBox
@Composable
fun FeedPage(
navController: NavHostController,
modifier: Modifier = Modifier,
viewModel: FeedViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
filter: Filter,
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
) {
val viewState = viewModel.viewState.collectAsStateValue()
val syncState = homeViewModel.syncState.collectAsStateValue()
LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state ->
viewModel.dispatch(
FeedViewAction.FetchData(
isStarred = state.filter.let { it != Filter.All && it == Filter.Starred },
isUnread = state.filter.let { it != Filter.All && it == Filter.Unread },
)
)
}
}
DisposableEffect(Unit) {
viewModel.dispatch(
FeedViewAction.FetchAccount()
)
onDispose { }
}
Box(
modifier = modifier.fillMaxSize()
) {
SubscribeDialog(
openInputStreamCallback = {
viewModel.dispatch(FeedViewAction.AddFromFile(it))
},
)
TopTitleBox(
title = viewState.account?.name ?: "未知账户",
description = if (syncState.isSyncing) {
"Syncing (${syncState.syncedCount}/${syncState.feedCount}) : ${syncState.currentFeedName}"
} else {
viewState.account?.updateAt?.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true)
?: "从未同步"
},
listState = viewState.listState,
startOffset = Offset(20f, 80f),
startHeight = 72f,
startTitleFontSize = 38f,
startDescriptionFontSize = 16f,
) {
viewModel.dispatch(FeedViewAction.ScrollToItem(0))
}
Column {
FeedPageTopBar(
navController = navController,
isSyncing = syncState.isSyncing,
syncOnClick = {
homeViewModel.dispatch(HomeViewAction.Sync())
},
subscribeOnClick = {
subscribeViewModel.dispatch(SubscribeViewAction.Show)
},
)
LazyColumn(
state = viewState.listState,
modifier = Modifier.weight(1f),
) {
item {
Spacer(modifier = Modifier.height(114.dp))
ButtonBar(
buttonBarType = ButtonBarType.FilterBar(
title = filter.title,
important = viewState.filterImportant,
),
onClick = {
groupAndFeedOnClick(null, null)
}
)
}
item {
Spacer(modifier = Modifier.height(10.dp))
ButtonBar(
buttonBarType = ButtonBarType.AllBar(
title = "Feeds",
icon = Icons.Rounded.ExpandMore
),
onClick = {
viewModel.dispatch(FeedViewAction.ChangeGroupVisible)
}
)
}
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
GroupList(
modifier = modifier,
groupVisible = viewState.groupsVisible,
feedVisible = viewState.feedsVisible[index],
groupWithFeed = groupWithFeed,
groupAndFeedOnClick = groupAndFeedOnClick,
expandOnClick = {
viewModel.dispatch(FeedViewAction.ChangeFeedVisible(index))
}
)
}
}
}
}
}

View File

@ -1,66 +0,0 @@
package me.ash.reader.ui.page.home.feed
import android.view.HapticFeedbackConstants
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Refresh
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
import androidx.navigation.NavHostController
import me.ash.reader.ui.page.common.RouteName
@Composable
fun FeedPageTopBar(
navController: NavHostController,
isSyncing: Boolean = false,
syncOnClick: () -> Unit = {},
subscribeOnClick: () -> Unit = {},
) {
val view = LocalView.current
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
navController.navigate(route = RouteName.SETTINGS)
}) {
Icon(
// modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
if (isSyncing) return@IconButton
syncOnClick()
}) {
Icon(
// modifier = Modifier.size(26.dp),
imageVector = Icons.Rounded.Refresh,
contentDescription = "Sync",
tint = MaterialTheme.colorScheme.primary,
)
}
IconButton(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
subscribeOnClick()
}) {
Icon(
// modifier = Modifier.size(26.dp),
imageVector = Icons.Rounded.Add,
contentDescription = "Subscribe",
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
}

View File

@ -1,49 +0,0 @@
package me.ash.reader.ui.page.home.feed
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
@Composable
fun ColumnScope.GroupList(
modifier: Modifier = Modifier,
groupVisible: Boolean,
feedVisible: Boolean,
groupWithFeed: GroupWithFeed,
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
expandOnClick: () -> Unit,
) {
AnimatedVisibility(
visible = groupVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column(modifier = modifier) {
ButtonBar(
buttonBarType = ButtonBarType.GroupBar(
title = groupWithFeed.group.name,
icon = Icons.Rounded.ExpandMore,
important = groupWithFeed.group.important ?: 0,
iconOnClick = expandOnClick,
),
onClick = {
groupAndFeedOnClick(groupWithFeed.group, null)
}
)
FeedList(
visible = feedVisible,
feeds = groupWithFeed.feeds,
onClick = { currentFeed ->
groupAndFeedOnClick(null, currentFeed)
}
)
}
}
}

View File

@ -0,0 +1,66 @@
package me.ash.reader.ui.page.home.feeds
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.Badge
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.clip
import androidx.compose.ui.unit.dp
@Composable
fun Feed(
modifier: Modifier = Modifier,
name: String,
important: Int,
onClick: () -> Unit = {},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.clip(RoundedCornerShape(32.dp))
.clickable { onClick() }
.padding(vertical = 14.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(modifier = Modifier.padding(start = 14.dp)) {
Row(
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.outline),
) {}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (important != 0) {
Badge(
modifier = Modifier.padding(end = 6.dp),
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = important.toString(),
style = MaterialTheme.typography.labelSmall
)
},
)
}
}
}
}

View File

@ -0,0 +1,201 @@
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.material.icons.Icons
import androidx.compose.material.icons.outlined.KeyboardArrowRight
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.*
import androidx.compose.runtime.Composable
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.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
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewAction
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.widget.Banner
import me.ash.reader.ui.widget.Subtitle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeedsPage(
modifier: Modifier = Modifier,
navController: NavHostController,
viewModel: FeedsViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
) {
val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue()
val syncState = homeViewModel.syncState.collectAsStateValue()
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing)
)
)
LaunchedEffect(Unit) {
viewModel.dispatch(FeedsViewAction.FetchAccount())
}
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 },
)
)
}
}
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
topBar = {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
IconButton(onClick = {
if (syncState.isSyncing) return@IconButton
homeViewModel.dispatch(HomeViewAction.Sync())
}) {
Icon(
modifier = Modifier.graphicsLayer {
rotationZ = if (syncState.isSyncing) angle else 0f
},
imageVector = Icons.Rounded.Refresh,
contentDescription = "Refresh",
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(onClick = {
subscribeViewModel.dispatch(SubscribeViewAction.Show)
}) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Subscribe",
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
)
},
content = {
SubscribeDialog(
openInputStreamCallback = {
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,
)
},
)
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,
)
)
}
)
if (index != viewState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
)
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feed package me.ash.reader.ui.page.home.feeds
import android.util.Log import android.util.Log
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.account.Account import me.ash.reader.data.account.Account
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.repository.AccountRepository import me.ash.reader.data.repository.AccountRepository
import me.ash.reader.data.repository.OpmlRepository import me.ash.reader.data.repository.OpmlRepository
@ -17,22 +18,20 @@ import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FeedViewModel @Inject constructor( class FeedsViewModel @Inject constructor(
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val opmlRepository: OpmlRepository, private val opmlRepository: OpmlRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(FeedViewState()) private val _viewState = MutableStateFlow(FeedsViewState())
val viewState: StateFlow<FeedViewState> = _viewState.asStateFlow() val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
fun dispatch(action: FeedViewAction) { fun dispatch(action: FeedsViewAction) {
when (action) { when (action) {
is FeedViewAction.FetchAccount -> fetchAccount(action.callback) is FeedsViewAction.FetchAccount -> fetchAccount(action.callback)
is FeedViewAction.FetchData -> fetchData(action.isStarred, action.isUnread) is FeedsViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
is FeedViewAction.AddFromFile -> addFromFile(action.inputStream) is FeedsViewAction.AddFromFile -> addFromFile(action.inputStream)
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index) is FeedsViewAction.ScrollToItem -> scrollToItem(action.index)
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible()
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
} }
} }
@ -57,6 +56,7 @@ class FeedViewModel @Inject constructor(
private fun fetchData(isStarred: Boolean, isUnread: Boolean) { private fun fetchData(isStarred: Boolean, isUnread: Boolean) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
pullFeeds(isStarred, isUnread) pullFeeds(isStarred, isUnread)
_viewState
} }
} }
@ -101,7 +101,13 @@ class FeedViewModel @Inject constructor(
}.onEach { groupWithFeedList -> }.onEach { groupWithFeedList ->
_viewState.update { _viewState.update {
it.copy( it.copy(
filterImportant = groupWithFeedList.sumOf { it.group.important ?: 0 }, filter = when {
isStarred -> Filter.Starred
isUnread -> Filter.Unread
else -> Filter.All
}.apply {
important = groupWithFeedList.sumOf { it.group.important ?: 0 }
},
groupWithFeedList = groupWithFeedList, groupWithFeedList = groupWithFeedList,
feedsVisible = List(groupWithFeedList.size, init = { true }) feedsVisible = List(groupWithFeedList.size, init = { true })
) )
@ -111,24 +117,6 @@ class FeedViewModel @Inject constructor(
}.collect() }.collect()
} }
private fun changeFeedVisible(index: Int) {
_viewState.update {
it.copy(
feedsVisible = _viewState.value.feedsVisible.toMutableList().apply {
this[index] = !this[index]
}
)
}
}
private fun changeGroupVisible() {
_viewState.update {
it.copy(
groupsVisible = !_viewState.value.groupsVisible
)
}
}
private fun scrollToItem(index: Int) { private fun scrollToItem(index: Int) {
viewModelScope.launch { viewModelScope.launch {
_viewState.value.listState.scrollToItem(index) _viewState.value.listState.scrollToItem(index)
@ -136,36 +124,30 @@ class FeedViewModel @Inject constructor(
} }
} }
data class FeedViewState( data class FeedsViewState(
val account: Account? = null, val account: Account? = null,
val filterImportant: Int = 0, val filter: Filter = Filter.All,
val groupWithFeedList: List<GroupWithFeed> = emptyList(), val groupWithFeedList: List<GroupWithFeed> = emptyList(),
val feedsVisible: List<Boolean> = emptyList(), val feedsVisible: List<Boolean> = emptyList(),
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true, val groupsVisible: Boolean = true,
) )
sealed class FeedViewAction { sealed class FeedsViewAction {
data class FetchData( data class FetchData(
val isStarred: Boolean, val isStarred: Boolean,
val isUnread: Boolean, val isUnread: Boolean,
) : FeedViewAction() ) : FeedsViewAction()
data class FetchAccount( data class FetchAccount(
val callback: () -> Unit = {}, val callback: () -> Unit = {},
) : FeedViewAction() ) : FeedsViewAction()
data class AddFromFile( data class AddFromFile(
val inputStream: InputStream val inputStream: InputStream
) : FeedViewAction() ) : FeedsViewAction()
data class ChangeFeedVisible(
val index: Int
) : FeedViewAction()
object ChangeGroupVisible : FeedViewAction()
data class ScrollToItem( data class ScrollToItem(
val index: Int val index: Int
) : FeedViewAction() ) : FeedsViewAction()
} }

View File

@ -0,0 +1,92 @@
package me.ash.reader.ui.page.home.feeds
import androidx.compose.animation.*
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.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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import me.ash.reader.data.feed.Feed
@Composable
fun Group(
modifier: Modifier = Modifier,
text: String,
feeds: List<Feed>,
isExpanded: Boolean = true,
groupOnClick: () -> Unit = {},
feedOnClick: (feed: Feed) -> Unit = {},
) {
var expanded by remember { mutableStateOf(isExpanded) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.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)
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(start = 28.dp),
text = text,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
Row(
modifier = Modifier
.padding(end = 20.dp)
.size(24.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f))
.clickable {
expanded = !expanded
},
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = if (expanded) "Expand Less" else "Expand More",
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column {
Spacer(modifier = Modifier.height(16.dp))
feeds.forEach { feed ->
Feed(
modifier = Modifier.padding(horizontal = 20.dp),
name = feed.name,
important = feed.important ?: 0,
) {
feedOnClick(feed)
}
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feed.subscribe package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feed.subscribe package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feed.subscribe package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feed.subscribe package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.page.home.feed.subscribe package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View File

@ -2,74 +2,61 @@ package me.ash.reader.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
//val md_theme_light_primary = Color(0xFF4D4D4D) val md_theme_light_primary = Color(0xFF00658e)
val md_theme_light_primary = Color(0xFF6750A4) val md_theme_light_onPrimary = Color(0xFFffffff)
val md_theme_light_onPrimary = Color(0xFFFFFFFF) val md_theme_light_primaryContainer = Color(0xFFc3e7ff)
val md_theme_light_primaryContainer = Color(0xFFEADDFF) val md_theme_light_onPrimaryContainer = Color(0xFF001e2e)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D) val md_theme_light_secondary = Color(0xFF4f616e)
val md_theme_light_onSecondary = Color(0xFFffffff)
//val md_theme_light_secondary = Color(0xFF868686) val md_theme_light_secondaryContainer = Color(0xFFd2e5f4)
val md_theme_light_secondary = Color(0xFF625B71) val md_theme_light_onSecondaryContainer = Color(0xFF0b1d28)
val md_theme_light_onSecondary = Color(0xFFFFFFFF) val md_theme_light_tertiary = Color(0xFF625a7c)
val md_theme_light_onTertiary = Color(0xFFffffff)
//val md_theme_light_secondaryContainer = Color(0xFFEAEAEA) val md_theme_light_tertiaryContainer = Color(0xFFe8ddff)
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) val md_theme_light_onTertiaryContainer = Color(0xFF1e1735)
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B) val md_theme_light_error = Color(0xFFba1b1b)
val md_theme_light_errorContainer = Color(0xFFffdad4)
//val md_theme_light_tertiary = Color(0xFFC1C1C1) val md_theme_light_onError = Color(0xFFffffff)
val md_theme_light_tertiary = Color(0xFF7D5260) val md_theme_light_onErrorContainer = Color(0xFF410001)
val md_theme_light_onTertiary = Color(0xFFFFFFFF) val md_theme_light_background = Color(0xFFfbfcff)
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4) val md_theme_light_onBackground = Color(0xFF191c1e)
val md_theme_light_onTertiaryContainer = Color(0xFF31111D) val md_theme_light_surface = Color(0xFFfbfcff)
val md_theme_light_error = Color(0xFFB3261E) val md_theme_light_onSurface = Color(0xFF191c1e)
val md_theme_light_errorContainer = Color(0xFFF9DEDC) val md_theme_light_surfaceVariant = Color(0xFFdde3ea)
val md_theme_light_onError = Color(0xFFFFFFFF) val md_theme_light_onSurfaceVariant = Color(0xFF41484d)
val md_theme_light_onErrorContainer = Color(0xFF410E0B) val md_theme_light_outline = Color(0xFF71787e)
val md_theme_light_inverseOnSurface = Color(0xFFf0f1f4)
//val md_theme_light_background = Color(0xFFF7F5F4) val md_theme_light_inverseSurface = Color(0xFF2e3133)
val md_theme_light_background = Color(0xFFFFFBFE) val md_theme_light_inversePrimary = Color(0xFF7fcfff)
val md_theme_light_onBackground = Color(0xFF1C1B1F)
//val md_theme_light_surface = Color(0xFFF7F5F4)
val md_theme_light_surface = Color(0xFFFFFBFE)
val md_theme_light_onSurface = Color(0xFF1C1B1F)
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
//val md_theme_light_onSurfaceVariant = md_theme_light_secondary
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
val md_theme_light_outline = Color(0xFF79747E)
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
val md_theme_light_inverseSurface = Color(0xFF313033)
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
val md_theme_light_shadow = Color(0xFF000000) val md_theme_light_shadow = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFD0BCFF) val md_theme_dark_primary = Color(0xFF7fcfff)
val md_theme_dark_onPrimary = Color(0xFF381E72) val md_theme_dark_onPrimary = Color(0xFF00344b)
val md_theme_dark_primaryContainer = Color(0xFF4F378B) val md_theme_dark_primaryContainer = Color(0xFF004c6c)
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF) val md_theme_dark_onPrimaryContainer = Color(0xFFc3e7ff)
val md_theme_dark_secondary = Color(0xFFCCC2DC) val md_theme_dark_secondary = Color(0xFFb6c9d8)
val md_theme_dark_onSecondary = Color(0xFF332D41) val md_theme_dark_onSecondary = Color(0xFF21333e)
val md_theme_dark_secondaryContainer = Color(0xFF4A4458) val md_theme_dark_secondaryContainer = Color(0xFF374955)
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8) val md_theme_dark_onSecondaryContainer = Color(0xFFd2e5f4)
val md_theme_dark_tertiary = Color(0xFFEFB8C8) val md_theme_dark_tertiary = Color(0xFFccc1e9)
val md_theme_dark_onTertiary = Color(0xFF492532) val md_theme_dark_onTertiary = Color(0xFF332c4b)
val md_theme_dark_tertiaryContainer = Color(0xFF633B48) val md_theme_dark_tertiaryContainer = Color(0xFF4a4263)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4) val md_theme_dark_onTertiaryContainer = Color(0xFFe8ddff)
val md_theme_dark_error = Color(0xFFF2B8B5) val md_theme_dark_error = Color(0xFFffb4a9)
val md_theme_dark_errorContainer = Color(0xFF8C1D18) val md_theme_dark_errorContainer = Color(0xFF930006)
val md_theme_dark_onError = Color(0xFF601410) val md_theme_dark_onError = Color(0xFF680003)
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC) val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
val md_theme_dark_background = Color(0xFF1C1B1F) val md_theme_dark_background = Color(0xFF191c1e)
val md_theme_dark_onBackground = Color(0xFFE6E1E5) val md_theme_dark_onBackground = Color(0xFFe1e2e5)
val md_theme_dark_surface = Color(0xFF1C1B1F) val md_theme_dark_surface = Color(0xFF191c1e)
val md_theme_dark_onSurface = Color(0xFFE6E1E5) val md_theme_dark_onSurface = Color(0xFFe1e2e5)
val md_theme_dark_surfaceVariant = Color(0xFF49454F) val md_theme_dark_surfaceVariant = Color(0xFF41484d)
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0) val md_theme_dark_onSurfaceVariant = Color(0xFFc1c7ce)
val md_theme_dark_outline = Color(0xFF938F99) val md_theme_dark_outline = Color(0xFF8b9298)
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F) val md_theme_dark_inverseOnSurface = Color(0xFF191c1e)
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5) val md_theme_dark_inverseSurface = Color(0xFFe1e2e5)
val md_theme_dark_inversePrimary = Color(0xFF6750A4) val md_theme_dark_inversePrimary = Color(0xFF00658e)
val md_theme_dark_shadow = Color(0xFF000000) val md_theme_dark_shadow = Color(0xFF000000)
val seed = Color(0xFF6750A4) val seed = Color(0xFF006187)
val error = Color(0xFFB3261E) val error = Color(0xFFba1b1b)

View File

@ -71,7 +71,7 @@ private val DarkThemeColors = darkColorScheme(
@Composable @Composable
fun AppTheme( fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit content: @Composable () -> Unit
) { ) {
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

View File

@ -2,114 +2,165 @@ package me.ash.reader.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.ash.reader.R
//Replace with your font locations val googleSansDisplay: FontFamily = FontFamily(
val Roboto = FontFamily.Default Font(
resId = R.font.google_sans_display_regular,
weight = FontWeight.Normal,
style = FontStyle.Normal
),
Font(
resId = R.font.google_sans_display_medium,
weight = FontWeight.Medium,
style = FontStyle.Normal
),
Font(
resId = R.font.google_sans_display_bold,
weight = FontWeight.Bold,
style = FontStyle.Normal
),
)
val googleSansText: FontFamily = FontFamily(
Font(
resId = R.font.google_sans_text_regular,
weight = FontWeight.Normal,
style = FontStyle.Normal
),
Font(
resId = R.font.google_sans_text_italic,
weight = FontWeight.Normal,
style = FontStyle.Italic
),
Font(
resId = R.font.google_sans_text_medium,
weight = FontWeight.Medium,
style = FontStyle.Normal
),
Font(
resId = R.font.google_sans_text_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
),
Font(
resId = R.font.google_sans_text_bold,
weight = FontWeight.Bold,
style = FontStyle.Normal
),
Font(
resId = R.font.google_sans_text_bold_italic,
weight = FontWeight.Bold,
style = FontStyle.Italic
),
)
val AppTypography = Typography( val AppTypography = Typography(
displayLarge = TextStyle( displayLarge = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 57.sp, fontSize = 57.sp,
lineHeight = 64.sp, lineHeight = 64.sp,
letterSpacing = -0.25.sp, letterSpacing = -0.25.sp,
), ),
displayMedium = TextStyle( displayMedium = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 45.sp, fontSize = 45.sp,
lineHeight = 52.sp, lineHeight = 52.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
displaySmall = TextStyle( displaySmall = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 36.sp, fontSize = 36.sp,
lineHeight = 44.sp, lineHeight = 44.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
headlineLarge = TextStyle( headlineLarge = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 40.sp, lineHeight = 40.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
headlineMedium = TextStyle( headlineMedium = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 28.sp, fontSize = 28.sp,
lineHeight = 36.sp, lineHeight = 36.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
headlineSmall = TextStyle( headlineSmall = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 24.sp, fontSize = 24.sp,
lineHeight = 32.sp, lineHeight = 32.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = Roboto, fontFamily = googleSansDisplay,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
titleSmall = TextStyle( titleSmall = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
labelLarge = TextStyle( labelLarge = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.25.sp, letterSpacing = 0.25.sp,
), ),
bodySmall = TextStyle( bodySmall = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.4.sp, letterSpacing = 0.4.sp,
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
labelSmall = TextStyle( labelSmall = TextStyle(
fontFamily = Roboto, fontFamily = googleSansText,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 11.sp, fontSize = 11.sp,
lineHeight = 16.sp, lineHeight = 16.sp,

View File

@ -0,0 +1,73 @@
package me.ash.reader.ui.widget
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.Surface
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.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun Banner(
modifier: Modifier = Modifier,
title: String,
desc: String? = null,
icon: ImageVector? = null,
onClick: () -> Unit = {},
action: (@Composable () -> Unit)? = null
) {
Surface(
modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable { onClick() }
.padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(end = 16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
maxLines = if (desc == null) 2 else 1,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
color = MaterialTheme.colorScheme.onSurface,
)
desc?.let {
Text(
text = it,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
}
}
action?.let {
Box(Modifier.padding(start = 16.dp)) {
it()
}
}
}
}
}

View File

@ -0,0 +1,26 @@
package me.ash.reader.ui.widget
import androidx.compose.foundation.layout.fillMaxWidth
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun Subtitle(
text: String,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary,
) {
Text(
text = text,
modifier = modifier
.fillMaxWidth()
.padding(24.dp, 8.dp, 16.dp, 8.dp),
color = color,
style = MaterialTheme.typography.labelLarge
)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.