Refactor Material You design for FeedsPage
This commit is contained in:
+1
-1
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 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()
|
||||
}
|
||||
@@ -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.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.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.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.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.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,
|
||||
|
||||
@@ -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