Refactor Material You design for FeedsPage
This commit is contained in:
+1
-1
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
+25
-43
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -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.*
|
||||||
+1
-1
@@ -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
|
||||||
+1
-1
@@ -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
|
||||||
+1
-1
@@ -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
|
||||||
+1
-1
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
Binary file not shown.
Reference in New Issue
Block a user