Refactor Material You design for FeedsPage
This commit is contained in:
parent
f99fc8698a
commit
d288177feb
|
@ -25,7 +25,7 @@ android {
|
|||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
// shrinkResources true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,39 @@
|
|||
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(
|
||||
var index: Int,
|
||||
var title: String,
|
||||
var description: String,
|
||||
var important: Int,
|
||||
var icon: ImageVector,
|
||||
) {
|
||||
companion object {
|
||||
val Starred = Filter(
|
||||
index = 0,
|
||||
title = "Starred",
|
||||
description = " Starred Items",
|
||||
important = 13
|
||||
important = 13,
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
)
|
||||
val Unread = Filter(
|
||||
index = 1,
|
||||
title = "Unread",
|
||||
description = " Unread Items",
|
||||
important = 666
|
||||
important = 666,
|
||||
icon = Icons.Outlined.FiberManualRecord,
|
||||
)
|
||||
val All = Filter(
|
||||
index = 2,
|
||||
title = "All",
|
||||
description = " Unread Items",
|
||||
important = 666
|
||||
important = 666,
|
||||
icon = Icons.Rounded.Subject,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ class AccountRepository @Inject constructor(
|
|||
|
||||
suspend fun addDefaultAccount(): Account {
|
||||
return Account(
|
||||
name = "Reader You",
|
||||
name = "Read You",
|
||||
type = Account.Type.LOCAL,
|
||||
).apply {
|
||||
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.findActivity
|
||||
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.ReadViewAction
|
||||
import me.ash.reader.ui.page.home.read.ReadViewModel
|
||||
|
@ -98,25 +98,8 @@ fun HomePage(
|
|||
state = viewState.pagerState,
|
||||
composableList = listOf(
|
||||
{
|
||||
FeedPage(
|
||||
FeedsPage(
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
66
app/src/main/java/me/ash/reader/ui/page/home/feeds/Feed.kt
Normal file
66
app/src/main/java/me/ash/reader/ui/page/home/feeds/Feed.kt
Normal 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
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
201
app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt
Normal file
201
app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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 androidx.compose.foundation.lazy.LazyListState
|
||||
|
@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
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.repository.AccountRepository
|
||||
import me.ash.reader.data.repository.OpmlRepository
|
||||
|
@ -17,22 +18,20 @@ import java.io.InputStream
|
|||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FeedViewModel @Inject constructor(
|
||||
class FeedsViewModel @Inject constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
private val opmlRepository: OpmlRepository,
|
||||
) : ViewModel() {
|
||||
private val _viewState = MutableStateFlow(FeedViewState())
|
||||
val viewState: StateFlow<FeedViewState> = _viewState.asStateFlow()
|
||||
private val _viewState = MutableStateFlow(FeedsViewState())
|
||||
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
|
||||
|
||||
fun dispatch(action: FeedViewAction) {
|
||||
fun dispatch(action: FeedsViewAction) {
|
||||
when (action) {
|
||||
is FeedViewAction.FetchAccount -> fetchAccount(action.callback)
|
||||
is FeedViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
|
||||
is FeedViewAction.AddFromFile -> addFromFile(action.inputStream)
|
||||
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
|
||||
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible()
|
||||
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||
is FeedsViewAction.FetchAccount -> fetchAccount(action.callback)
|
||||
is FeedsViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
|
||||
is FeedsViewAction.AddFromFile -> addFromFile(action.inputStream)
|
||||
is FeedsViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,6 +56,7 @@ class FeedViewModel @Inject constructor(
|
|||
private fun fetchData(isStarred: Boolean, isUnread: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
pullFeeds(isStarred, isUnread)
|
||||
_viewState
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,13 @@ class FeedViewModel @Inject constructor(
|
|||
}.onEach { groupWithFeedList ->
|
||||
_viewState.update {
|
||||
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,
|
||||
feedsVisible = List(groupWithFeedList.size, init = { true })
|
||||
)
|
||||
|
@ -111,24 +117,6 @@ class FeedViewModel @Inject constructor(
|
|||
}.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) {
|
||||
viewModelScope.launch {
|
||||
_viewState.value.listState.scrollToItem(index)
|
||||
|
@ -136,36 +124,30 @@ class FeedViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
data class FeedViewState(
|
||||
data class FeedsViewState(
|
||||
val account: Account? = null,
|
||||
val filterImportant: Int = 0,
|
||||
val filter: Filter = Filter.All,
|
||||
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
|
||||
val feedsVisible: List<Boolean> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val groupsVisible: Boolean = true,
|
||||
)
|
||||
|
||||
sealed class FeedViewAction {
|
||||
sealed class FeedsViewAction {
|
||||
data class FetchData(
|
||||
val isStarred: Boolean,
|
||||
val isUnread: Boolean,
|
||||
) : FeedViewAction()
|
||||
) : FeedsViewAction()
|
||||
|
||||
data class FetchAccount(
|
||||
val callback: () -> Unit = {},
|
||||
) : FeedViewAction()
|
||||
) : FeedsViewAction()
|
||||
|
||||
data class AddFromFile(
|
||||
val inputStream: InputStream
|
||||
) : FeedViewAction()
|
||||
|
||||
data class ChangeFeedVisible(
|
||||
val index: Int
|
||||
) : FeedViewAction()
|
||||
|
||||
object ChangeGroupVisible : FeedViewAction()
|
||||
) : FeedsViewAction()
|
||||
|
||||
data class ScrollToItem(
|
||||
val index: Int
|
||||
) : FeedViewAction()
|
||||
) : FeedsViewAction()
|
||||
}
|
92
app/src/main/java/me/ash/reader/ui/page/home/feeds/Group.kt
Normal file
92
app/src/main/java/me/ash/reader/ui/page/home/feeds/Group.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.foundation.layout.*
|
|
@ -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.Spacer
|
|
@ -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.result.contract.ActivityResultContracts
|
|
@ -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.viewModelScope
|
|
@ -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.runtime.Composable
|
|
@ -2,74 +2,61 @@ package me.ash.reader.ui.theme
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
//val md_theme_light_primary = Color(0xFF4D4D4D)
|
||||
val md_theme_light_primary = Color(0xFF6750A4)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
|
||||
|
||||
//val md_theme_light_secondary = Color(0xFF868686)
|
||||
val md_theme_light_secondary = Color(0xFF625B71)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
|
||||
//val md_theme_light_secondaryContainer = Color(0xFFEAEAEA)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
|
||||
|
||||
//val md_theme_light_tertiary = Color(0xFFC1C1C1)
|
||||
val md_theme_light_tertiary = Color(0xFF7D5260)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
|
||||
val md_theme_light_error = Color(0xFFB3261E)
|
||||
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
|
||||
|
||||
//val md_theme_light_background = Color(0xFFF7F5F4)
|
||||
val md_theme_light_background = Color(0xFFFFFBFE)
|
||||
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_primary = Color(0xFF00658e)
|
||||
val md_theme_light_onPrimary = Color(0xFFffffff)
|
||||
val md_theme_light_primaryContainer = Color(0xFFc3e7ff)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF001e2e)
|
||||
val md_theme_light_secondary = Color(0xFF4f616e)
|
||||
val md_theme_light_onSecondary = Color(0xFFffffff)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFd2e5f4)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF0b1d28)
|
||||
val md_theme_light_tertiary = Color(0xFF625a7c)
|
||||
val md_theme_light_onTertiary = Color(0xFFffffff)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFe8ddff)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF1e1735)
|
||||
val md_theme_light_error = Color(0xFFba1b1b)
|
||||
val md_theme_light_errorContainer = Color(0xFFffdad4)
|
||||
val md_theme_light_onError = Color(0xFFffffff)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410001)
|
||||
val md_theme_light_background = Color(0xFFfbfcff)
|
||||
val md_theme_light_onBackground = Color(0xFF191c1e)
|
||||
val md_theme_light_surface = Color(0xFFfbfcff)
|
||||
val md_theme_light_onSurface = Color(0xFF191c1e)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFdde3ea)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF41484d)
|
||||
val md_theme_light_outline = Color(0xFF71787e)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFf0f1f4)
|
||||
val md_theme_light_inverseSurface = Color(0xFF2e3133)
|
||||
val md_theme_light_inversePrimary = Color(0xFF7fcfff)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFD0BCFF)
|
||||
val md_theme_dark_onPrimary = Color(0xFF381E72)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
|
||||
val md_theme_dark_secondary = Color(0xFFCCC2DC)
|
||||
val md_theme_dark_onSecondary = Color(0xFF332D41)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
|
||||
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
|
||||
val md_theme_dark_onTertiary = Color(0xFF492532)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
|
||||
val md_theme_dark_error = Color(0xFFF2B8B5)
|
||||
val md_theme_dark_errorContainer = Color(0xFF8C1D18)
|
||||
val md_theme_dark_onError = Color(0xFF601410)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
|
||||
val md_theme_dark_background = Color(0xFF1C1B1F)
|
||||
val md_theme_dark_onBackground = Color(0xFFE6E1E5)
|
||||
val md_theme_dark_surface = Color(0xFF1C1B1F)
|
||||
val md_theme_dark_onSurface = Color(0xFFE6E1E5)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
|
||||
val md_theme_dark_outline = Color(0xFF938F99)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
|
||||
val md_theme_dark_primary = Color(0xFF7fcfff)
|
||||
val md_theme_dark_onPrimary = Color(0xFF00344b)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF004c6c)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFc3e7ff)
|
||||
val md_theme_dark_secondary = Color(0xFFb6c9d8)
|
||||
val md_theme_dark_onSecondary = Color(0xFF21333e)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF374955)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFd2e5f4)
|
||||
val md_theme_dark_tertiary = Color(0xFFccc1e9)
|
||||
val md_theme_dark_onTertiary = Color(0xFF332c4b)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF4a4263)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFe8ddff)
|
||||
val md_theme_dark_error = Color(0xFFffb4a9)
|
||||
val md_theme_dark_errorContainer = Color(0xFF930006)
|
||||
val md_theme_dark_onError = Color(0xFF680003)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
|
||||
val md_theme_dark_background = Color(0xFF191c1e)
|
||||
val md_theme_dark_onBackground = Color(0xFFe1e2e5)
|
||||
val md_theme_dark_surface = Color(0xFF191c1e)
|
||||
val md_theme_dark_onSurface = Color(0xFFe1e2e5)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF41484d)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFc1c7ce)
|
||||
val md_theme_dark_outline = Color(0xFF8b9298)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF191c1e)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFe1e2e5)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF00658e)
|
||||
val md_theme_dark_shadow = Color(0xFF000000)
|
||||
|
||||
val seed = Color(0xFF6750A4)
|
||||
val error = Color(0xFFB3261E)
|
||||
val seed = Color(0xFF006187)
|
||||
val error = Color(0xFFba1b1b)
|
|
@ -71,7 +71,7 @@ private val DarkThemeColors = darkColorScheme(
|
|||
@Composable
|
||||
fun AppTheme(
|
||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable() () -> Unit
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
// Dynamic color is available on Android 12+
|
||||
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.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
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.unit.sp
|
||||
import me.ash.reader.R
|
||||
|
||||
//Replace with your font locations
|
||||
val Roboto = FontFamily.Default
|
||||
val googleSansDisplay: FontFamily = FontFamily(
|
||||
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(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = -0.25.sp,
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansDisplay,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp,
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp,
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = Roboto,
|
||||
fontFamily = googleSansText,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
|
|
73
app/src/main/java/me/ash/reader/ui/widget/Banner.kt
Normal file
73
app/src/main/java/me/ash/reader/ui/widget/Banner.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
app/src/main/java/me/ash/reader/ui/widget/SubTitle.kt
Normal file
26
app/src/main/java/me/ash/reader/ui/widget/SubTitle.kt
Normal 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
|
||||
)
|
||||
}
|
BIN
app/src/main/res/font/google_sans_display_bold.ttf
Normal file
BIN
app/src/main/res/font/google_sans_display_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_display_medium.ttf
Normal file
BIN
app/src/main/res/font/google_sans_display_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_display_regular.ttf
Normal file
BIN
app/src/main/res/font/google_sans_display_regular.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_text_bold.ttf
Normal file
BIN
app/src/main/res/font/google_sans_text_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_text_bold_italic.ttf
Normal file
BIN
app/src/main/res/font/google_sans_text_bold_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_text_italic.ttf
Normal file
BIN
app/src/main/res/font/google_sans_text_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_text_medium.ttf
Normal file
BIN
app/src/main/res/font/google_sans_text_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_text_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/google_sans_text_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/google_sans_text_regular.ttf
Normal file
BIN
app/src/main/res/font/google_sans_text_regular.ttf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user