Refactor Material You design for FeedsPage
This commit is contained in:
parent
f99fc8698a
commit
d288177feb
|
@ -25,7 +25,7 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
// shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,39 @@
|
||||||
package me.ash.reader.data.constant
|
package me.ash.reader.data.constant
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.FiberManualRecord
|
||||||
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
|
import androidx.compose.material.icons.rounded.Subject
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
class Filter(
|
class Filter(
|
||||||
var index: Int,
|
var index: Int,
|
||||||
var title: String,
|
var title: String,
|
||||||
var description: String,
|
var description: String,
|
||||||
var important: Int,
|
var important: Int,
|
||||||
|
var icon: ImageVector,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Starred = Filter(
|
val Starred = Filter(
|
||||||
index = 0,
|
index = 0,
|
||||||
title = "Starred",
|
title = "Starred",
|
||||||
description = " Starred Items",
|
description = " Starred Items",
|
||||||
important = 13
|
important = 13,
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
)
|
)
|
||||||
val Unread = Filter(
|
val Unread = Filter(
|
||||||
index = 1,
|
index = 1,
|
||||||
title = "Unread",
|
title = "Unread",
|
||||||
description = " Unread Items",
|
description = " Unread Items",
|
||||||
important = 666
|
important = 666,
|
||||||
|
icon = Icons.Outlined.FiberManualRecord,
|
||||||
)
|
)
|
||||||
val All = Filter(
|
val All = Filter(
|
||||||
index = 2,
|
index = 2,
|
||||||
title = "All",
|
title = "All",
|
||||||
description = " Unread Items",
|
description = " Unread Items",
|
||||||
important = 666
|
important = 666,
|
||||||
|
icon = Icons.Rounded.Subject,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ class AccountRepository @Inject constructor(
|
||||||
|
|
||||||
suspend fun addDefaultAccount(): Account {
|
suspend fun addDefaultAccount(): Account {
|
||||||
return Account(
|
return Account(
|
||||||
name = "Reader You",
|
name = "Read You",
|
||||||
type = Account.Type.LOCAL,
|
type = Account.Type.LOCAL,
|
||||||
).apply {
|
).apply {
|
||||||
id = accountDao.insert(this).toInt()
|
id = accountDao.insert(this).toInt()
|
||||||
|
|
|
@ -20,7 +20,7 @@ import me.ash.reader.data.constant.Symbol
|
||||||
import me.ash.reader.ui.extension.collectAsStateValue
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.extension.findActivity
|
import me.ash.reader.ui.extension.findActivity
|
||||||
import me.ash.reader.ui.page.home.article.ArticlePage
|
import me.ash.reader.ui.page.home.article.ArticlePage
|
||||||
import me.ash.reader.ui.page.home.feed.FeedPage
|
import me.ash.reader.ui.page.home.feeds.FeedsPage
|
||||||
import me.ash.reader.ui.page.home.read.ReadPage
|
import me.ash.reader.ui.page.home.read.ReadPage
|
||||||
import me.ash.reader.ui.page.home.read.ReadViewAction
|
import me.ash.reader.ui.page.home.read.ReadViewAction
|
||||||
import me.ash.reader.ui.page.home.read.ReadViewModel
|
import me.ash.reader.ui.page.home.read.ReadViewModel
|
||||||
|
@ -98,25 +98,8 @@ fun HomePage(
|
||||||
state = viewState.pagerState,
|
state = viewState.pagerState,
|
||||||
composableList = listOf(
|
composableList = listOf(
|
||||||
{
|
{
|
||||||
FeedPage(
|
FeedsPage(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
filter = filterState.filter,
|
|
||||||
groupAndFeedOnClick = { currentGroup, currentFeed ->
|
|
||||||
viewModel.dispatch(
|
|
||||||
HomeViewAction.ChangeFilter(
|
|
||||||
filterState.copy(
|
|
||||||
group = currentGroup,
|
|
||||||
feed = currentFeed,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
viewModel.dispatch(
|
|
||||||
HomeViewAction.ScrollToPage(
|
|
||||||
scope = scope,
|
|
||||||
targetPage = 1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,231 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.LocalIndication
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.indication
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.PressInteraction
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.ash.reader.R
|
|
||||||
import me.ash.reader.ui.widget.AnimatedText
|
|
||||||
import me.ash.reader.ui.widget.Menu
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ButtonBar(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
buttonBarType: ButtonBarType,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
var offset by remember { mutableStateOf(DpOffset.Zero) }
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val view = LocalView.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(50.dp)
|
|
||||||
.indication(interactionSource, LocalIndication.current)
|
|
||||||
.padding(horizontal = 20.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = { offset ->
|
|
||||||
val press = PressInteraction.Press(offset)
|
|
||||||
interactionSource.emit(press)
|
|
||||||
tryAwaitRelease()
|
|
||||||
interactionSource.emit(PressInteraction.Release(press))
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
onClick()
|
|
||||||
},
|
|
||||||
onLongPress = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
offset = DpOffset(it.x.toDp(), it.y.toDp())
|
|
||||||
expanded = true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
when (buttonBarType) {
|
|
||||||
is ButtonBarType.FilterBar -> FilterBar(buttonBarType)
|
|
||||||
is ButtonBarType.AllBar -> AllBar(buttonBarType)
|
|
||||||
is ButtonBarType.GroupBar -> GroupBar(buttonBarType)
|
|
||||||
is ButtonBarType.FeedBar -> FeedBar(buttonBarType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu(
|
|
||||||
offset = offset,
|
|
||||||
expanded = expanded,
|
|
||||||
dismissFunction = { expanded = false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FilterBar(
|
|
||||||
buttonBarType: ButtonBarType.FilterBar,
|
|
||||||
) {
|
|
||||||
AnimatedText(
|
|
||||||
text = buttonBarType.title,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
AnimatedText(
|
|
||||||
text = buttonBarType.important.toString(),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AllBar(
|
|
||||||
buttonBarType: ButtonBarType.AllBar,
|
|
||||||
) {
|
|
||||||
AnimatedText(
|
|
||||||
text = buttonBarType.title,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
imageVector = buttonBarType.icon,
|
|
||||||
contentDescription = "Expand More",
|
|
||||||
tint = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupBar(
|
|
||||||
buttonBarType: ButtonBarType.GroupBar,
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Icon(
|
|
||||||
imageVector = buttonBarType.icon,
|
|
||||||
contentDescription = "icon",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.clickable(onClick = buttonBarType.iconOnClick),
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 20.dp),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
text = buttonBarType.title,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AnimatedText(
|
|
||||||
text = buttonBarType.important.toString(),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FeedBar(
|
|
||||||
buttonBarType: ButtonBarType.FeedBar,
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 28.dp, end = 4.dp)
|
|
||||||
.size(24.dp),
|
|
||||||
//.background(MaterialTheme.colorScheme.inversePrimary),
|
|
||||||
color = if (buttonBarType.icon == null) {
|
|
||||||
MaterialTheme.colorScheme.inversePrimary
|
|
||||||
} else {
|
|
||||||
Color.Unspecified
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (buttonBarType.icon == null) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.default_folder),
|
|
||||||
contentDescription = "icon",
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Image(
|
|
||||||
painter = buttonBarType.icon,
|
|
||||||
contentDescription = "icon",
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 20.dp),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
text = buttonBarType.title,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AnimatedText(
|
|
||||||
text = buttonBarType.important.toString(),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ButtonBarType {
|
|
||||||
data class FilterBar(
|
|
||||||
val modifier: Modifier = Modifier,
|
|
||||||
val title: String = "",
|
|
||||||
val important: Int = 0,
|
|
||||||
) : ButtonBarType()
|
|
||||||
|
|
||||||
data class AllBar(
|
|
||||||
val title: String = "",
|
|
||||||
val icon: ImageVector,
|
|
||||||
) : ButtonBarType()
|
|
||||||
|
|
||||||
data class GroupBar(
|
|
||||||
val title: String = "",
|
|
||||||
val important: Int = 0,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val iconOnClick: () -> Unit = {},
|
|
||||||
) : ButtonBarType()
|
|
||||||
|
|
||||||
data class FeedBar(
|
|
||||||
val title: String = "",
|
|
||||||
val important: Int = 0,
|
|
||||||
val icon: Painter? = null,
|
|
||||||
) : ButtonBarType()
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
|
||||||
import me.ash.reader.data.feed.Feed
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ColumnScope.FeedList(
|
|
||||||
visible: Boolean,
|
|
||||||
feeds: List<Feed>,
|
|
||||||
onClick: (currentFeed: Feed?) -> Unit = {},
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = visible,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.animateContentSize()) {
|
|
||||||
feeds.forEach { feed ->
|
|
||||||
ButtonBar(
|
|
||||||
buttonBarType = ButtonBarType.FeedBar(
|
|
||||||
title = feed.name,
|
|
||||||
important = feed.important ?: 0,
|
|
||||||
icon = if (feed.icon == null) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
BitmapPainter(
|
|
||||||
BitmapFactory.decodeByteArray(
|
|
||||||
feed.icon,
|
|
||||||
0,
|
|
||||||
feed.icon!!.size
|
|
||||||
).asImageBitmap()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
onClick(feed)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.ExpandMore
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import me.ash.reader.DateTimeExt
|
|
||||||
import me.ash.reader.DateTimeExt.toString
|
|
||||||
import me.ash.reader.data.constant.Filter
|
|
||||||
import me.ash.reader.data.feed.Feed
|
|
||||||
import me.ash.reader.data.group.Group
|
|
||||||
import me.ash.reader.ui.extension.collectAsStateValue
|
|
||||||
import me.ash.reader.ui.page.home.HomeViewAction
|
|
||||||
import me.ash.reader.ui.page.home.HomeViewModel
|
|
||||||
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeDialog
|
|
||||||
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeViewAction
|
|
||||||
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeViewModel
|
|
||||||
import me.ash.reader.ui.widget.TopTitleBox
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FeedPage(
|
|
||||||
navController: NavHostController,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: FeedViewModel = hiltViewModel(),
|
|
||||||
homeViewModel: HomeViewModel = hiltViewModel(),
|
|
||||||
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
|
|
||||||
filter: Filter,
|
|
||||||
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
|
||||||
) {
|
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
|
||||||
val syncState = homeViewModel.syncState.collectAsStateValue()
|
|
||||||
|
|
||||||
LaunchedEffect(homeViewModel.filterState) {
|
|
||||||
homeViewModel.filterState.collect { state ->
|
|
||||||
viewModel.dispatch(
|
|
||||||
FeedViewAction.FetchData(
|
|
||||||
isStarred = state.filter.let { it != Filter.All && it == Filter.Starred },
|
|
||||||
isUnread = state.filter.let { it != Filter.All && it == Filter.Unread },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
viewModel.dispatch(
|
|
||||||
FeedViewAction.FetchAccount()
|
|
||||||
)
|
|
||||||
onDispose { }
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
SubscribeDialog(
|
|
||||||
openInputStreamCallback = {
|
|
||||||
viewModel.dispatch(FeedViewAction.AddFromFile(it))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
TopTitleBox(
|
|
||||||
title = viewState.account?.name ?: "未知账户",
|
|
||||||
description = if (syncState.isSyncing) {
|
|
||||||
"Syncing (${syncState.syncedCount}/${syncState.feedCount}) : ${syncState.currentFeedName}"
|
|
||||||
} else {
|
|
||||||
viewState.account?.updateAt?.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true)
|
|
||||||
?: "从未同步"
|
|
||||||
},
|
|
||||||
listState = viewState.listState,
|
|
||||||
startOffset = Offset(20f, 80f),
|
|
||||||
startHeight = 72f,
|
|
||||||
startTitleFontSize = 38f,
|
|
||||||
startDescriptionFontSize = 16f,
|
|
||||||
) {
|
|
||||||
viewModel.dispatch(FeedViewAction.ScrollToItem(0))
|
|
||||||
}
|
|
||||||
Column {
|
|
||||||
FeedPageTopBar(
|
|
||||||
navController = navController,
|
|
||||||
isSyncing = syncState.isSyncing,
|
|
||||||
syncOnClick = {
|
|
||||||
homeViewModel.dispatch(HomeViewAction.Sync())
|
|
||||||
},
|
|
||||||
subscribeOnClick = {
|
|
||||||
subscribeViewModel.dispatch(SubscribeViewAction.Show)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
LazyColumn(
|
|
||||||
state = viewState.listState,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(114.dp))
|
|
||||||
ButtonBar(
|
|
||||||
buttonBarType = ButtonBarType.FilterBar(
|
|
||||||
title = filter.title,
|
|
||||||
important = viewState.filterImportant,
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
groupAndFeedOnClick(null, null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
ButtonBar(
|
|
||||||
buttonBarType = ButtonBarType.AllBar(
|
|
||||||
title = "Feeds",
|
|
||||||
icon = Icons.Rounded.ExpandMore
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
viewModel.dispatch(FeedViewAction.ChangeGroupVisible)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
|
|
||||||
GroupList(
|
|
||||||
modifier = modifier,
|
|
||||||
groupVisible = viewState.groupsVisible,
|
|
||||||
feedVisible = viewState.feedsVisible[index],
|
|
||||||
groupWithFeed = groupWithFeed,
|
|
||||||
groupAndFeedOnClick = groupAndFeedOnClick,
|
|
||||||
expandOnClick = {
|
|
||||||
viewModel.dispatch(FeedViewAction.ChangeFeedVisible(index))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import me.ash.reader.ui.page.common.RouteName
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FeedPageTopBar(
|
|
||||||
navController: NavHostController,
|
|
||||||
isSyncing: Boolean = false,
|
|
||||||
syncOnClick: () -> Unit = {},
|
|
||||||
subscribeOnClick: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
val view = LocalView.current
|
|
||||||
SmallTopAppBar(
|
|
||||||
title = {},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
navController.navigate(route = RouteName.SETTINGS)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
// modifier = Modifier.size(22.dp),
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
if (isSyncing) return@IconButton
|
|
||||||
syncOnClick()
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
// modifier = Modifier.size(26.dp),
|
|
||||||
imageVector = Icons.Rounded.Refresh,
|
|
||||||
contentDescription = "Sync",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
subscribeOnClick()
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
// modifier = Modifier.size(26.dp),
|
|
||||||
imageVector = Icons.Rounded.Add,
|
|
||||||
contentDescription = "Subscribe",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home.feed
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.ExpandMore
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import me.ash.reader.data.feed.Feed
|
|
||||||
import me.ash.reader.data.group.Group
|
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ColumnScope.GroupList(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
groupVisible: Boolean,
|
|
||||||
feedVisible: Boolean,
|
|
||||||
groupWithFeed: GroupWithFeed,
|
|
||||||
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
|
||||||
expandOnClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = groupVisible,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
ButtonBar(
|
|
||||||
buttonBarType = ButtonBarType.GroupBar(
|
|
||||||
title = groupWithFeed.group.name,
|
|
||||||
icon = Icons.Rounded.ExpandMore,
|
|
||||||
important = groupWithFeed.group.important ?: 0,
|
|
||||||
iconOnClick = expandOnClick,
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
groupAndFeedOnClick(groupWithFeed.group, null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
FeedList(
|
|
||||||
visible = feedVisible,
|
|
||||||
feeds = groupWithFeed.feeds,
|
|
||||||
onClick = { currentFeed ->
|
|
||||||
groupAndFeedOnClick(null, currentFeed)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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 android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.account.Account
|
import me.ash.reader.data.account.Account
|
||||||
|
import me.ash.reader.data.constant.Filter
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
import me.ash.reader.data.repository.AccountRepository
|
import me.ash.reader.data.repository.AccountRepository
|
||||||
import me.ash.reader.data.repository.OpmlRepository
|
import me.ash.reader.data.repository.OpmlRepository
|
||||||
|
@ -17,22 +18,20 @@ import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FeedViewModel @Inject constructor(
|
class FeedsViewModel @Inject constructor(
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlRepository: OpmlRepository,
|
private val opmlRepository: OpmlRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _viewState = MutableStateFlow(FeedViewState())
|
private val _viewState = MutableStateFlow(FeedsViewState())
|
||||||
val viewState: StateFlow<FeedViewState> = _viewState.asStateFlow()
|
val viewState: StateFlow<FeedsViewState> = _viewState.asStateFlow()
|
||||||
|
|
||||||
fun dispatch(action: FeedViewAction) {
|
fun dispatch(action: FeedsViewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is FeedViewAction.FetchAccount -> fetchAccount(action.callback)
|
is FeedsViewAction.FetchAccount -> fetchAccount(action.callback)
|
||||||
is FeedViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
|
is FeedsViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
|
||||||
is FeedViewAction.AddFromFile -> addFromFile(action.inputStream)
|
is FeedsViewAction.AddFromFile -> addFromFile(action.inputStream)
|
||||||
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
|
is FeedsViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||||
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible()
|
|
||||||
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +56,7 @@ class FeedViewModel @Inject constructor(
|
||||||
private fun fetchData(isStarred: Boolean, isUnread: Boolean) {
|
private fun fetchData(isStarred: Boolean, isUnread: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
pullFeeds(isStarred, isUnread)
|
pullFeeds(isStarred, isUnread)
|
||||||
|
_viewState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,13 @@ class FeedViewModel @Inject constructor(
|
||||||
}.onEach { groupWithFeedList ->
|
}.onEach { groupWithFeedList ->
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
filterImportant = groupWithFeedList.sumOf { it.group.important ?: 0 },
|
filter = when {
|
||||||
|
isStarred -> Filter.Starred
|
||||||
|
isUnread -> Filter.Unread
|
||||||
|
else -> Filter.All
|
||||||
|
}.apply {
|
||||||
|
important = groupWithFeedList.sumOf { it.group.important ?: 0 }
|
||||||
|
},
|
||||||
groupWithFeedList = groupWithFeedList,
|
groupWithFeedList = groupWithFeedList,
|
||||||
feedsVisible = List(groupWithFeedList.size, init = { true })
|
feedsVisible = List(groupWithFeedList.size, init = { true })
|
||||||
)
|
)
|
||||||
|
@ -111,24 +117,6 @@ class FeedViewModel @Inject constructor(
|
||||||
}.collect()
|
}.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeFeedVisible(index: Int) {
|
|
||||||
_viewState.update {
|
|
||||||
it.copy(
|
|
||||||
feedsVisible = _viewState.value.feedsVisible.toMutableList().apply {
|
|
||||||
this[index] = !this[index]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeGroupVisible() {
|
|
||||||
_viewState.update {
|
|
||||||
it.copy(
|
|
||||||
groupsVisible = !_viewState.value.groupsVisible
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollToItem(index: Int) {
|
private fun scrollToItem(index: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_viewState.value.listState.scrollToItem(index)
|
_viewState.value.listState.scrollToItem(index)
|
||||||
|
@ -136,36 +124,30 @@ class FeedViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FeedViewState(
|
data class FeedsViewState(
|
||||||
val account: Account? = null,
|
val account: Account? = null,
|
||||||
val filterImportant: Int = 0,
|
val filter: Filter = Filter.All,
|
||||||
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
|
val groupWithFeedList: List<GroupWithFeed> = emptyList(),
|
||||||
val feedsVisible: List<Boolean> = emptyList(),
|
val feedsVisible: List<Boolean> = emptyList(),
|
||||||
val listState: LazyListState = LazyListState(),
|
val listState: LazyListState = LazyListState(),
|
||||||
val groupsVisible: Boolean = true,
|
val groupsVisible: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class FeedViewAction {
|
sealed class FeedsViewAction {
|
||||||
data class FetchData(
|
data class FetchData(
|
||||||
val isStarred: Boolean,
|
val isStarred: Boolean,
|
||||||
val isUnread: Boolean,
|
val isUnread: Boolean,
|
||||||
) : FeedViewAction()
|
) : FeedsViewAction()
|
||||||
|
|
||||||
data class FetchAccount(
|
data class FetchAccount(
|
||||||
val callback: () -> Unit = {},
|
val callback: () -> Unit = {},
|
||||||
) : FeedViewAction()
|
) : FeedsViewAction()
|
||||||
|
|
||||||
data class AddFromFile(
|
data class AddFromFile(
|
||||||
val inputStream: InputStream
|
val inputStream: InputStream
|
||||||
) : FeedViewAction()
|
) : FeedsViewAction()
|
||||||
|
|
||||||
data class ChangeFeedVisible(
|
|
||||||
val index: Int
|
|
||||||
) : FeedViewAction()
|
|
||||||
|
|
||||||
object ChangeGroupVisible : FeedViewAction()
|
|
||||||
|
|
||||||
data class ScrollToItem(
|
data class ScrollToItem(
|
||||||
val index: Int
|
val index: Int
|
||||||
) : FeedViewAction()
|
) : FeedsViewAction()
|
||||||
}
|
}
|
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.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.*
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
|
@ -2,74 +2,61 @@ package me.ash.reader.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
//val md_theme_light_primary = Color(0xFF4D4D4D)
|
val md_theme_light_primary = Color(0xFF00658e)
|
||||||
val md_theme_light_primary = Color(0xFF6750A4)
|
val md_theme_light_onPrimary = Color(0xFFffffff)
|
||||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
val md_theme_light_primaryContainer = Color(0xFFc3e7ff)
|
||||||
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
|
val md_theme_light_onPrimaryContainer = Color(0xFF001e2e)
|
||||||
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
|
val md_theme_light_secondary = Color(0xFF4f616e)
|
||||||
|
val md_theme_light_onSecondary = Color(0xFFffffff)
|
||||||
//val md_theme_light_secondary = Color(0xFF868686)
|
val md_theme_light_secondaryContainer = Color(0xFFd2e5f4)
|
||||||
val md_theme_light_secondary = Color(0xFF625B71)
|
val md_theme_light_onSecondaryContainer = Color(0xFF0b1d28)
|
||||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
val md_theme_light_tertiary = Color(0xFF625a7c)
|
||||||
|
val md_theme_light_onTertiary = Color(0xFFffffff)
|
||||||
//val md_theme_light_secondaryContainer = Color(0xFFEAEAEA)
|
val md_theme_light_tertiaryContainer = Color(0xFFe8ddff)
|
||||||
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
|
val md_theme_light_onTertiaryContainer = Color(0xFF1e1735)
|
||||||
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
|
val md_theme_light_error = Color(0xFFba1b1b)
|
||||||
|
val md_theme_light_errorContainer = Color(0xFFffdad4)
|
||||||
//val md_theme_light_tertiary = Color(0xFFC1C1C1)
|
val md_theme_light_onError = Color(0xFFffffff)
|
||||||
val md_theme_light_tertiary = Color(0xFF7D5260)
|
val md_theme_light_onErrorContainer = Color(0xFF410001)
|
||||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
val md_theme_light_background = Color(0xFFfbfcff)
|
||||||
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
|
val md_theme_light_onBackground = Color(0xFF191c1e)
|
||||||
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
|
val md_theme_light_surface = Color(0xFFfbfcff)
|
||||||
val md_theme_light_error = Color(0xFFB3261E)
|
val md_theme_light_onSurface = Color(0xFF191c1e)
|
||||||
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
|
val md_theme_light_surfaceVariant = Color(0xFFdde3ea)
|
||||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
val md_theme_light_onSurfaceVariant = Color(0xFF41484d)
|
||||||
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
|
val md_theme_light_outline = Color(0xFF71787e)
|
||||||
|
val md_theme_light_inverseOnSurface = Color(0xFFf0f1f4)
|
||||||
//val md_theme_light_background = Color(0xFFF7F5F4)
|
val md_theme_light_inverseSurface = Color(0xFF2e3133)
|
||||||
val md_theme_light_background = Color(0xFFFFFBFE)
|
val md_theme_light_inversePrimary = Color(0xFF7fcfff)
|
||||||
val md_theme_light_onBackground = Color(0xFF1C1B1F)
|
|
||||||
|
|
||||||
//val md_theme_light_surface = Color(0xFFF7F5F4)
|
|
||||||
val md_theme_light_surface = Color(0xFFFFFBFE)
|
|
||||||
val md_theme_light_onSurface = Color(0xFF1C1B1F)
|
|
||||||
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
|
|
||||||
|
|
||||||
//val md_theme_light_onSurfaceVariant = md_theme_light_secondary
|
|
||||||
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
|
|
||||||
val md_theme_light_outline = Color(0xFF79747E)
|
|
||||||
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
|
|
||||||
val md_theme_light_inverseSurface = Color(0xFF313033)
|
|
||||||
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
|
|
||||||
val md_theme_light_shadow = Color(0xFF000000)
|
val md_theme_light_shadow = Color(0xFF000000)
|
||||||
|
|
||||||
val md_theme_dark_primary = Color(0xFFD0BCFF)
|
val md_theme_dark_primary = Color(0xFF7fcfff)
|
||||||
val md_theme_dark_onPrimary = Color(0xFF381E72)
|
val md_theme_dark_onPrimary = Color(0xFF00344b)
|
||||||
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
|
val md_theme_dark_primaryContainer = Color(0xFF004c6c)
|
||||||
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
|
val md_theme_dark_onPrimaryContainer = Color(0xFFc3e7ff)
|
||||||
val md_theme_dark_secondary = Color(0xFFCCC2DC)
|
val md_theme_dark_secondary = Color(0xFFb6c9d8)
|
||||||
val md_theme_dark_onSecondary = Color(0xFF332D41)
|
val md_theme_dark_onSecondary = Color(0xFF21333e)
|
||||||
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
|
val md_theme_dark_secondaryContainer = Color(0xFF374955)
|
||||||
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
|
val md_theme_dark_onSecondaryContainer = Color(0xFFd2e5f4)
|
||||||
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
|
val md_theme_dark_tertiary = Color(0xFFccc1e9)
|
||||||
val md_theme_dark_onTertiary = Color(0xFF492532)
|
val md_theme_dark_onTertiary = Color(0xFF332c4b)
|
||||||
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
|
val md_theme_dark_tertiaryContainer = Color(0xFF4a4263)
|
||||||
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
|
val md_theme_dark_onTertiaryContainer = Color(0xFFe8ddff)
|
||||||
val md_theme_dark_error = Color(0xFFF2B8B5)
|
val md_theme_dark_error = Color(0xFFffb4a9)
|
||||||
val md_theme_dark_errorContainer = Color(0xFF8C1D18)
|
val md_theme_dark_errorContainer = Color(0xFF930006)
|
||||||
val md_theme_dark_onError = Color(0xFF601410)
|
val md_theme_dark_onError = Color(0xFF680003)
|
||||||
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
|
val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
|
||||||
val md_theme_dark_background = Color(0xFF1C1B1F)
|
val md_theme_dark_background = Color(0xFF191c1e)
|
||||||
val md_theme_dark_onBackground = Color(0xFFE6E1E5)
|
val md_theme_dark_onBackground = Color(0xFFe1e2e5)
|
||||||
val md_theme_dark_surface = Color(0xFF1C1B1F)
|
val md_theme_dark_surface = Color(0xFF191c1e)
|
||||||
val md_theme_dark_onSurface = Color(0xFFE6E1E5)
|
val md_theme_dark_onSurface = Color(0xFFe1e2e5)
|
||||||
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
|
val md_theme_dark_surfaceVariant = Color(0xFF41484d)
|
||||||
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
|
val md_theme_dark_onSurfaceVariant = Color(0xFFc1c7ce)
|
||||||
val md_theme_dark_outline = Color(0xFF938F99)
|
val md_theme_dark_outline = Color(0xFF8b9298)
|
||||||
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
|
val md_theme_dark_inverseOnSurface = Color(0xFF191c1e)
|
||||||
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
|
val md_theme_dark_inverseSurface = Color(0xFFe1e2e5)
|
||||||
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
|
val md_theme_dark_inversePrimary = Color(0xFF00658e)
|
||||||
val md_theme_dark_shadow = Color(0xFF000000)
|
val md_theme_dark_shadow = Color(0xFF000000)
|
||||||
|
|
||||||
val seed = Color(0xFF6750A4)
|
val seed = Color(0xFF006187)
|
||||||
val error = Color(0xFFB3261E)
|
val error = Color(0xFFba1b1b)
|
|
@ -71,7 +71,7 @@ private val DarkThemeColors = darkColorScheme(
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTheme(
|
fun AppTheme(
|
||||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
content: @Composable() () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
// Dynamic color is available on Android 12+
|
// Dynamic color is available on Android 12+
|
||||||
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
|
|
|
@ -2,114 +2,165 @@ package me.ash.reader.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.ash.reader.R
|
||||||
|
|
||||||
//Replace with your font locations
|
val googleSansDisplay: FontFamily = FontFamily(
|
||||||
val Roboto = FontFamily.Default
|
Font(
|
||||||
|
resId = R.font.google_sans_display_regular,
|
||||||
|
weight = FontWeight.Normal,
|
||||||
|
style = FontStyle.Normal
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_display_medium,
|
||||||
|
weight = FontWeight.Medium,
|
||||||
|
style = FontStyle.Normal
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_display_bold,
|
||||||
|
weight = FontWeight.Bold,
|
||||||
|
style = FontStyle.Normal
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val googleSansText: FontFamily = FontFamily(
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_text_regular,
|
||||||
|
weight = FontWeight.Normal,
|
||||||
|
style = FontStyle.Normal
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_text_italic,
|
||||||
|
weight = FontWeight.Normal,
|
||||||
|
style = FontStyle.Italic
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_text_medium,
|
||||||
|
weight = FontWeight.Medium,
|
||||||
|
style = FontStyle.Normal
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_text_medium_italic,
|
||||||
|
weight = FontWeight.Medium,
|
||||||
|
style = FontStyle.Italic
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_text_bold,
|
||||||
|
weight = FontWeight.Bold,
|
||||||
|
style = FontStyle.Normal
|
||||||
|
),
|
||||||
|
Font(
|
||||||
|
resId = R.font.google_sans_text_bold_italic,
|
||||||
|
weight = FontWeight.Bold,
|
||||||
|
style = FontStyle.Italic
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
val AppTypography = Typography(
|
val AppTypography = Typography(
|
||||||
displayLarge = TextStyle(
|
displayLarge = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 57.sp,
|
fontSize = 57.sp,
|
||||||
lineHeight = 64.sp,
|
lineHeight = 64.sp,
|
||||||
letterSpacing = -0.25.sp,
|
letterSpacing = -0.25.sp,
|
||||||
),
|
),
|
||||||
displayMedium = TextStyle(
|
displayMedium = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 45.sp,
|
fontSize = 45.sp,
|
||||||
lineHeight = 52.sp,
|
lineHeight = 52.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
displaySmall = TextStyle(
|
displaySmall = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 36.sp,
|
fontSize = 36.sp,
|
||||||
lineHeight = 44.sp,
|
lineHeight = 44.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
headlineLarge = TextStyle(
|
headlineLarge = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 32.sp,
|
fontSize = 32.sp,
|
||||||
lineHeight = 40.sp,
|
lineHeight = 40.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
headlineMedium = TextStyle(
|
headlineMedium = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 28.sp,
|
fontSize = 28.sp,
|
||||||
lineHeight = 36.sp,
|
lineHeight = 36.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
headlineSmall = TextStyle(
|
headlineSmall = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
lineHeight = 32.sp,
|
lineHeight = 32.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
titleLarge = TextStyle(
|
titleLarge = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansDisplay,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
lineHeight = 28.sp,
|
lineHeight = 28.sp,
|
||||||
letterSpacing = 0.sp,
|
letterSpacing = 0.sp,
|
||||||
),
|
),
|
||||||
titleMedium = TextStyle(
|
titleMedium = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.1.sp,
|
letterSpacing = 0.1.sp,
|
||||||
),
|
),
|
||||||
titleSmall = TextStyle(
|
titleSmall = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.1.sp,
|
letterSpacing = 0.1.sp,
|
||||||
),
|
),
|
||||||
labelLarge = TextStyle(
|
labelLarge = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.1.sp,
|
letterSpacing = 0.1.sp,
|
||||||
),
|
),
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
bodyMedium = TextStyle(
|
bodyMedium = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.25.sp,
|
letterSpacing = 0.25.sp,
|
||||||
),
|
),
|
||||||
bodySmall = TextStyle(
|
bodySmall = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.W400,
|
fontWeight = FontWeight.W400,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.4.sp,
|
letterSpacing = 0.4.sp,
|
||||||
),
|
),
|
||||||
labelMedium = TextStyle(
|
labelMedium = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
labelSmall = TextStyle(
|
labelSmall = TextStyle(
|
||||||
fontFamily = Roboto,
|
fontFamily = googleSansText,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
|
|
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