Min SDK upgraded to 26 and Full screen when scrolling down in the ReadPage

This commit is contained in:
Ash 2022-03-29 17:22:35 +08:00
parent 67b033f1ec
commit 11d839fff4
18 changed files with 631 additions and 636 deletions

View File

@ -11,7 +11,7 @@ android {
defaultConfig { defaultConfig {
applicationId "me.ash.reader" applicationId "me.ash.reader"
minSdk 24 minSdk 26
targetSdk 32 targetSdk 32
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@ -50,6 +50,7 @@ android {
} }
dependencies { dependencies {
implementation("io.coil-kt:coil-compose:2.0.0-rc02")
implementation("androidx.compose.animation:animation-graphics:$compose_version") implementation("androidx.compose.animation:animation-graphics:$compose_version")
implementation("com.google.accompanist:accompanist-flowlayout:0.24.3-alpha") implementation("com.google.accompanist:accompanist-flowlayout:0.24.3-alpha")
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha") implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha")

View File

@ -6,6 +6,43 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ArticleDao { interface ArticleDao {
@Query(
"""
UPDATE article SET isUnread = 0
WHERE accountId = :accountId
AND isUnread = 1
AND date <= :before
"""
)
suspend fun markAllAsRead(accountId: Int, before: Long)
@Query(
"""
UPDATE article SET isUnread = 0
WHERE accountId = :accountId
AND isUnread = 1
AND date <= :before
AND feedId = :feedId
"""
)
suspend fun markAllAsReadByFeedId(accountId: Int, before: Long, feedId: String)
//
// @Query(
// """
// UPDATE article SET isUnread = 0
// WHERE accountId = :accountId
// AND isUnread = 1
// AND date <= :before
// AND feedId = :feedId
//
// SELECT * FROM `group` AS a, feed AS b, article AS c
// WHERE a.accountId = :accountId
// AND a.id = b.groupId
// AND b.groupId = :groupId
// AND c.feedId = b.id
// """
// )
// suspend fun markAllAsReadByGroupId(accountId: Int, before: Long, groupId: String)
@Query( @Query(
""" """

View File

@ -0,0 +1,58 @@
package me.ash.reader.ui.page.home
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.data.constant.Filter
import me.ash.reader.ui.extension.getName
@OptIn(ExperimentalPagerApi::class)
@Composable
fun FilterBar(
modifier: Modifier = Modifier,
filter: Filter,
filterOnClick: (Filter) -> Unit = {},
) {
Box(
modifier = Modifier.height(60.dp)
) {
Divider(
modifier = Modifier.fillMaxWidth().height(1.dp).zIndex(1f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
NavigationBar(
modifier = Modifier.fillMaxSize(),
tonalElevation = 0.dp,
) {
Spacer(modifier = Modifier.width(60.dp))
listOf(
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
NavigationBarItem(
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.getName()
)
},
selected = filter == item,
onClick = { filterOnClick(item) },
// colors = NavigationBarItemDefaults.colors(
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
// unselectedIconColor = MaterialTheme.colorScheme.outline,
// selectedTextColor = MaterialTheme.colorScheme.onSurface,
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
// indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
// )
)
}
Spacer(modifier = Modifier.width(60.dp))
}
}
}

View File

@ -1,50 +0,0 @@
package me.ash.reader.ui.page.home
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.data.constant.Filter
import me.ash.reader.ui.extension.getName
@OptIn(ExperimentalPagerApi::class)
@Composable
fun FilterBar2(
modifier: Modifier = Modifier,
filter: Filter,
onSelected: (Filter) -> Unit = {},
) {
NavigationBar(
tonalElevation = 0.dp,
) {
Spacer(modifier = Modifier.width(60.dp))
listOf(
Filter.Starred,
Filter.Unread,
Filter.All,
).forEach { item ->
NavigationBarItem(
icon = {
Icon(
imageVector = item.icon,
contentDescription = item.getName()
)
},
// label = { Text(text = item.getName()) },
selected = filter == item,
onClick = { onSelected(item) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
unselectedIconColor = MaterialTheme.colorScheme.outline,
selectedTextColor = MaterialTheme.colorScheme.onSurface,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
)
)
}
Spacer(modifier = Modifier.width(60.dp))
}
}

View File

@ -1,409 +0,0 @@
package me.ash.reader.ui.page.home
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.outlined.TextFormat
import androidx.compose.material.icons.rounded.Article
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.Divider
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.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowCrossAxisAlignment
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import com.google.accompanist.flowlayout.SizeMode
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import me.ash.reader.R
import me.ash.reader.data.constant.Filter
import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.theme.LocalLightThemeColors
import me.ash.reader.ui.widget.CanBeDisabledIconButton
import kotlin.math.absoluteValue
@OptIn(ExperimentalPagerApi::class)
@Composable
fun HomeBottomNavBar(
modifier: Modifier = Modifier,
pagerState: PagerState,
filter: Filter,
filterOnClick: (Filter) -> Unit = {},
disabled: Boolean,
isUnread: Boolean,
isStarred: Boolean,
isFullContent: Boolean,
unreadOnClick: (afterIsUnread: Boolean) -> Unit = {},
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
val transition = updateTransition(targetState = pagerState, label = "")
val readerBarAlpha by transition.animateFloat(
label = "",
transitionSpec = {
tween(
easing = FastOutLinearInEasing,
)
}
) {
if (it.currentPage < 2) {
if (it.currentPage == it.targetPage) {
0f
} else {
if (it.targetPage == 2) {
it.currentPageOffset.absoluteValue
} else {
0f
}
}
} else {
if (it.currentPage == it.targetPage) {
1f
} else {
if (it.targetPage == 1) {
1f - it.currentPageOffset.absoluteValue
} else {
0f
}
}
}
}
Divider(
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.surface)
) {
AnimatedVisibility(
visible = readerBarAlpha < 1f,
enter = fadeIn(),
exit = fadeOut(),
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.animateContentSize()
.alpha(1 - readerBarAlpha),
) {
Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}")
// FilterBar(
// modifier = modifier,
// filter = filter,
// onSelected = filterOnClick,
// )
FilterBar2(
modifier = modifier,
filter = filter,
onSelected = filterOnClick,
)
}
}
AnimatedVisibility(
visible = readerBarAlpha > 0f,
enter = fadeIn(),
exit = fadeOut(),
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.animateContentSize()
.alpha(readerBarAlpha),
) {
ReaderBar(
modifier = Modifier,
disabled = disabled,
isUnread = isUnread,
isStarred = isStarred,
isFullContent = isFullContent,
unreadOnClick = unreadOnClick,
starredOnClick = starredOnClick,
fullContentOnClick = fullContentOnClick,
)
}
}
}
}
@Composable
private fun FilterBar(
modifier: Modifier = Modifier,
filter: Filter,
onSelected: (Filter) -> Unit = {},
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
FlowRow(
mainAxisSize = SizeMode.Expand,
mainAxisAlignment = MainAxisAlignment.Center,
crossAxisAlignment = FlowCrossAxisAlignment.Center,
crossAxisSpacing = 0.dp,
mainAxisSpacing = 20.dp,
) {
listOf(
Filter.Starred,
Filter.Unread,
Filter.All
).forEach { item ->
Item(
icon = if (filter == item) item.filledIcon else item.icon,
name = item.getName(),
selected = filter == item,
) {
onSelected(item)
}
}
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Item(
modifier: Modifier = Modifier,
icon: ImageVector,
name: String,
selected: Boolean = false,
onClick: () -> Unit = {},
) {
val view = LocalView.current
val lightThemeColors = LocalLightThemeColors.current
val lightPrimaryContainer = lightThemeColors.primaryContainer
val lightOnSurface = lightThemeColors.onSurface
FilterChip(
modifier = Modifier
.height(36.dp)
.animateContentSize(),
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.outline,
leadingIconColor = MaterialTheme.colorScheme.outline,
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
selectedBackgroundColor = lightPrimaryContainer,
selectedContentColor = lightOnSurface,
selectedLeadingIconColor = lightOnSurface
),
selected = selected,
selectedIcon = {
Icon(
imageVector = icon,
contentDescription = name,
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = lightOnSurface,
)
},
onClick = {
view.playSoundEffect(SoundEffectConstants.CLICK)
onClick()
},
content = {
if (selected) {
Text(
modifier = modifier.padding(
start = 0.dp,
top = 8.dp,
end = 8.dp,
bottom = 8.dp
),
text = if (selected) name.uppercase() else "",
style = MaterialTheme.typography.titleSmall,
color = if (selected) {
lightOnSurface
} else {
MaterialTheme.colorScheme.outline
},
)
} else {
Icon(
imageVector = icon,
contentDescription = name,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.outline,
)
}
},
)
// Row(
// modifier = Modifier
// .animateContentSize()
// .height(40.dp)
// .width(if (selected) Dp.Unspecified else 40.dp)
// .padding(vertical = if (selected) 2.dp else 0.dp)
// .clip(CircleShape)
// .pointerInput(Unit) {
// detectTapGestures(
// onTap = {
// view.playSoundEffect(SoundEffectConstants.CLICK)
// onClick()
// }
// )
// }
// .background(
// if (selected) {
// MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.54f)
// } else {
// Color.Transparent
// }
// ),
// horizontalArrangement = Arrangement.Center,
// verticalAlignment = Alignment.CenterVertically,
// ) {
// Spacer(modifier = Modifier.width(8.dp))
// Icon(
// modifier = Modifier.size(20.dp),
// imageVector = icon,
// contentDescription = name,
// tint = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
// )
// if (selected) {
// Spacer(modifier = Modifier.width(8.dp))
// Text(
// modifier = Modifier.padding(horizontal = 8.dp),
// text = name.uppercase(),
// style = MaterialTheme.typography.titleSmall,
// color = if (selected) {
// MaterialTheme.colorScheme.onSurface
// } else {
// MaterialTheme.colorScheme.outline
// },
// )
// Spacer(modifier = Modifier.width(8.dp))
// }
// }
}
@Composable
private fun ReaderBar(
modifier: Modifier = Modifier,
disabled: Boolean,
isUnread: Boolean,
isStarred: Boolean,
isFullContent: Boolean,
unreadOnClick: (afterIsUnread: Boolean) -> Unit = {},
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
val view = LocalView.current
var fullContent by remember { mutableStateOf(isFullContent) }
Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.fillMaxWidth()
) {
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isUnread) {
Icons.Filled.FiberManualRecord
} else {
Icons.Outlined.FiberManualRecord
},
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = if (isUnread) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
unreadOnClick(!isUnread)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isStarred) {
Icons.Rounded.Star
} else {
Icons.Rounded.StarOutline
},
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = if (isStarred) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
starredOnClick(!isStarred)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(40.dp),
imageVector = Icons.Rounded.ExpandMore,
contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = Icons.Outlined.TextFormat,
contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(40.dp),
imageVector = if (fullContent) {
Icons.Rounded.Article
} else {
Icons.Outlined.Article
},
contentDescription = stringResource(R.string.parse_full_content),
tint = if (fullContent) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val afterIsFullContent = !fullContent
fullContent = afterIsFullContent
fullContentOnClick(afterIsFullContent)
}
}
}

View File

@ -3,13 +3,10 @@ package me.ash.reader.ui.page.home
import android.util.Log import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
@ -34,8 +31,6 @@ fun HomePage(
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val filterState = viewModel.filterState.collectAsStateValue()
val readState = readViewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
OpenArticleByExtras(extrasArticleId) OpenArticleByExtras(extrasArticleId)
@ -86,38 +81,6 @@ fun HomePage(
}, },
), ),
) )
HomeBottomNavBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
pagerState = viewState.pagerState,
disabled = readState.articleWithFeed == null,
isUnread = readState.articleWithFeed?.article?.isUnread ?: false,
isStarred = readState.articleWithFeed?.article?.isStarred ?: false,
isFullContent = readState.articleWithFeed?.feed?.isFullContent ?: false,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
},
starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it))
},
fullContentOnClick = { afterIsFullContent ->
readState.articleWithFeed?.let {
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
}
},
filter = filterState.filter,
filterOnClick = {
viewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
filter = it
)
)
)
},
)
} }
FeedOptionDrawer() FeedOptionDrawer()

View File

@ -3,10 +3,7 @@ package me.ash.reader.ui.page.home.feeds
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -29,6 +26,7 @@ import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getDesc import me.ash.reader.ui.extension.getDesc
import me.ash.reader.ui.extension.getName import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel 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.SubscribeDialog
@ -37,17 +35,17 @@ import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.widget.Banner import me.ash.reader.ui.widget.Banner
import me.ash.reader.ui.widget.Subtitle import me.ash.reader.ui.widget.Subtitle
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class)
@Composable @Composable
fun FeedsPage( fun FeedsPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navController: NavHostController, navController: NavHostController,
viewModel: FeedsViewModel = hiltViewModel(), feedsViewModel: FeedsViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
subscribeViewModel: SubscribeViewModel = hiltViewModel(), subscribeViewModel: SubscribeViewModel = hiltViewModel(),
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue() val viewState = feedsViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
val syncState = homeViewModel.syncState.collectAsStateValue() val syncState = homeViewModel.syncState.collectAsStateValue()
@ -61,12 +59,12 @@ fun FeedsPage(
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.dispatch(FeedsViewAction.FetchAccount) feedsViewModel.dispatch(FeedsViewAction.FetchAccount)
} }
LaunchedEffect(homeViewModel.filterState) { LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state -> homeViewModel.filterState.collect { state ->
viewModel.dispatch( feedsViewModel.dispatch(
FeedsViewAction.FetchData(state) FeedsViewAction.FetchData(state)
) )
} }
@ -114,7 +112,7 @@ fun FeedsPage(
content = { content = {
SubscribeDialog( SubscribeDialog(
openInputStreamCallback = { openInputStreamCallback = {
viewModel.dispatch(FeedsViewAction.AddFromFile(it)) feedsViewModel.dispatch(FeedsViewAction.AddFromFile(it))
}, },
) )
LazyColumn { LazyColumn {
@ -213,9 +211,27 @@ fun FeedsPage(
} }
} }
item { item {
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(64.dp))
Spacer(modifier = Modifier.height(64.dp))
} }
} }
},
bottomBar = {
FilterBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
filter = filterState.filter,
filterOnClick = {
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
filter = it
)
)
)
},
)
} }
) )
} }

View File

@ -32,12 +32,16 @@ import java.io.InputStream
@Composable @Composable
fun SubscribeDialog( fun SubscribeDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SubscribeViewModel = hiltViewModel(), subscribeViewModel: SubscribeViewModel = hiltViewModel(),
openInputStreamCallback: (InputStream) -> Unit, openInputStreamCallback: (InputStream) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = subscribeViewModel.viewState.collectAsStateValue()
val groupsState =
viewState.groups.collectAsState(initial = emptyList(), context = Dispatchers.IO)
var dialogHeight by remember { mutableStateOf(300.dp) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri -> it?.let { uri ->
context.contentResolver.openInputStream(uri)?.let { inputStream -> context.contentResolver.openInputStream(uri)?.let { inputStream ->
@ -45,21 +49,18 @@ fun SubscribeDialog(
} }
} }
} }
val viewState = viewModel.viewState.collectAsStateValue()
val groupsState =
viewState.groups.collectAsState(initial = emptyList(), context = Dispatchers.IO)
var dialogHeight by remember { mutableStateOf(300.dp) }
val readYouString = stringResource(R.string.read_you) val readYouString = stringResource(R.string.read_you)
val defaultString = stringResource(R.string.defaults) val defaultString = stringResource(R.string.defaults)
LaunchedEffect(viewState.visible) { LaunchedEffect(viewState.visible) {
if (viewState.visible) { if (viewState.visible) {
val defaultGroupId = context.dataStore val defaultGroupId = context.dataStore
.get(DataStoreKeys.CurrentAccountId)!! .get(DataStoreKeys.CurrentAccountId)!!
.spacerDollar(readYouString + defaultString) .spacerDollar(readYouString + defaultString)
viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId)) subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId))
viewModel.dispatch(SubscribeViewAction.Init) subscribeViewModel.dispatch(SubscribeViewAction.Init)
} else { } else {
viewModel.dispatch(SubscribeViewAction.Reset) subscribeViewModel.dispatch(SubscribeViewAction.Reset)
viewState.pagerState.scrollToPage(0) viewState.pagerState.scrollToPage(0)
} }
} }
@ -80,7 +81,7 @@ fun SubscribeDialog(
properties = DialogProperties(usePlatformDefaultWidth = false), properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = { onDismissRequest = {
focusManager.clearFocus() focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide) subscribeViewModel.dispatch(SubscribeViewAction.Hide)
}, },
icon = { icon = {
Icon( Icon(
@ -102,10 +103,10 @@ fun SubscribeDialog(
inputLink = viewState.linkContent, inputLink = viewState.linkContent,
errorMessage = viewState.errorMessage, errorMessage = viewState.errorMessage,
onLinkValueChange = { onLinkValueChange = {
viewModel.dispatch(SubscribeViewAction.InputLink(it)) subscribeViewModel.dispatch(SubscribeViewAction.InputLink(it))
}, },
onSearchKeyboardAction = { onSearchKeyboardAction = {
viewModel.dispatch(SubscribeViewAction.Search(scope)) subscribeViewModel.dispatch(SubscribeViewAction.Search(scope))
}, },
link = viewState.linkContent, link = viewState.linkContent,
groups = groupsState.value, groups = groupsState.value,
@ -114,24 +115,24 @@ fun SubscribeDialog(
selectedGroupId = viewState.selectedGroupId, selectedGroupId = viewState.selectedGroupId,
newGroupContent = viewState.newGroupContent, newGroupContent = viewState.newGroupContent,
onNewGroupValueChange = { onNewGroupValueChange = {
viewModel.dispatch(SubscribeViewAction.InputNewGroup(it)) subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it))
}, },
newGroupSelected = viewState.newGroupSelected, newGroupSelected = viewState.newGroupSelected,
changeNewGroupSelected = { changeNewGroupSelected = {
viewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it)) subscribeViewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it))
}, },
pagerState = viewState.pagerState, pagerState = viewState.pagerState,
allowNotificationPresetOnClick = { allowNotificationPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) subscribeViewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
}, },
parseFullContentPresetOnClick = { parseFullContentPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset) subscribeViewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset)
}, },
groupOnClick = { groupOnClick = {
viewModel.dispatch(SubscribeViewAction.SelectedGroup(it)) subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(it))
}, },
onResultKeyboardAction = { onResultKeyboardAction = {
viewModel.dispatch(SubscribeViewAction.Subscribe) subscribeViewModel.dispatch(SubscribeViewAction.Subscribe)
} }
) )
}, },
@ -142,7 +143,7 @@ fun SubscribeDialog(
enabled = viewState.linkContent.isNotEmpty(), enabled = viewState.linkContent.isNotEmpty(),
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Search(scope)) subscribeViewModel.dispatch(SubscribeViewAction.Search(scope))
} }
) { ) {
Text( Text(
@ -159,7 +160,7 @@ fun SubscribeDialog(
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Subscribe) subscribeViewModel.dispatch(SubscribeViewAction.Subscribe)
} }
) { ) {
Text(stringResource(R.string.subscribe)) Text(stringResource(R.string.subscribe))
@ -174,7 +175,7 @@ fun SubscribeDialog(
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
launcher.launch("*/*") launcher.launch("*/*")
viewModel.dispatch(SubscribeViewAction.Hide) subscribeViewModel.dispatch(SubscribeViewAction.Hide)
} }
) { ) {
Text(text = stringResource(R.string.import_from_opml)) Text(text = stringResource(R.string.import_from_opml))
@ -184,7 +185,7 @@ fun SubscribeDialog(
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide) subscribeViewModel.dispatch(SubscribeViewAction.Hide)
} }
) { ) {
Text(text = stringResource(R.string.cancel)) Text(text = stringResource(R.string.cancel))

View File

@ -25,13 +25,14 @@ fun ArticleItem(
onClick: (ArticleWithFeed) -> Unit = {}, onClick: (ArticleWithFeed) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable { onClick(articleWithFeed) } .clickable { onClick(articleWithFeed) }
.padding(horizontal = 12.dp, vertical = 12.dp) .padding(horizontal = 12.dp, vertical = 12.dp)
.alpha(if (articleWithFeed.article.isUnread) 1f else 0.5f), .alpha(if (articleWithFeed.article.isStarred || articleWithFeed.article.isUnread) 1f else 0.5f),
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View File

@ -5,6 +5,7 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -12,9 +13,7 @@ import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -26,6 +25,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getName import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.page.home.FilterBar
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.read.ReadViewModel import me.ash.reader.ui.page.home.read.ReadViewModel
@ -47,6 +47,7 @@ fun FlowPage(
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems() val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
var markAsRead by remember { mutableStateOf(false) }
LaunchedEffect(homeViewModel.filterState) { LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state -> homeViewModel.filterState.collect { state ->
@ -132,6 +133,24 @@ fun FlowPage(
generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope) generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
} }
} }
},
bottomBar = {
FilterBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
filter = filterState.filter,
filterOnClick = {
markAsRead = false
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
filter = it
)
)
)
},
)
} }
) )
} }

View File

@ -0,0 +1,83 @@
package me.ash.reader.ui.page.home.flow
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.res.stringResource
import androidx.compose.ui.unit.dp
import me.ash.reader.R
@Composable
fun MarkAsReadBar() {
Row(
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
MarkAsReadBarItem(
modifier = Modifier.weight(1f),
text = stringResource(R.string.seven_days),
)
MarkAsReadBarItem(
modifier = Modifier.weight(1f),
text = stringResource(R.string.three_days),
)
MarkAsReadBarItem(
modifier = Modifier.weight(1f),
text = stringResource(R.string.one_day),
)
MarkAsReadBarItem(
modifier = Modifier.weight(2.5f),
text = stringResource(R.string.mark_all_as_read),
isPrimary = true,
)
}
}
@Composable
fun MarkAsReadBarItem(
modifier: Modifier = Modifier,
text: String,
isPrimary: Boolean = false,
) {
Surface(
modifier = modifier
.height(52.dp)
.clip(RoundedCornerShape(16.dp))
.clickable { },
tonalElevation = 2.dp,
shape = RoundedCornerShape(16.dp),
color = if (isPrimary) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surface
}
) {
Box(
modifier = Modifier
.fillMaxHeight(),
contentAlignment = Alignment.Center,
) {
Text(
text = text,
style = MaterialTheme.typography.titleSmall,
color = if (isPrimary) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.secondary
},
)
}
}
if (!isPrimary) {
Spacer(modifier = Modifier.width(8.dp))
}
}

View File

@ -0,0 +1,139 @@
package me.ash.reader.ui.page.home.read
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.outlined.TextFormat
import androidx.compose.material.icons.rounded.Article
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import me.ash.reader.R
import me.ash.reader.ui.widget.CanBeDisabledIconButton
@Composable
fun ReadBar(
modifier: Modifier = Modifier,
disabled: Boolean,
isUnread: Boolean,
isStarred: Boolean,
isFullContent: Boolean,
unreadOnClick: (afterIsUnread: Boolean) -> Unit = {},
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
val view = LocalView.current
var fullContent by remember { mutableStateOf(isFullContent) }
Surface(
tonalElevation = 0.dp,
) {
Box(
modifier = Modifier.height(60.dp)
) {
Box {
Divider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.zIndex(1f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
}
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isUnread) {
Icons.Filled.FiberManualRecord
} else {
Icons.Outlined.FiberManualRecord
},
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = if (isUnread) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
unreadOnClick(!isUnread)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isStarred) {
Icons.Rounded.Star
} else {
Icons.Rounded.StarOutline
},
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = if (isStarred) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
starredOnClick(!isStarred)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(40.dp),
imageVector = Icons.Rounded.ExpandMore,
contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = Icons.Outlined.TextFormat,
contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(40.dp),
imageVector = if (fullContent) {
Icons.Rounded.Article
} else {
Icons.Outlined.Article
},
contentDescription = stringResource(R.string.parse_full_content),
tint = if (fullContent) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val afterIsFullContent = !fullContent
fullContent = afterIsFullContent
fullContentOnClick(afterIsFullContent)
}
}
}
}
}

View File

@ -1,6 +1,8 @@
package me.ash.reader.ui.page.home.read package me.ash.reader.ui.page.home.read
import androidx.compose.animation.Crossfade import android.content.Context
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -9,24 +11,22 @@ import androidx.compose.material.icons.outlined.Headphones
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.airbnb.lottie.compose.LottieAnimation import kotlinx.coroutines.CoroutineScope
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.widget.LottieAnimation
import me.ash.reader.ui.widget.WebView import me.ash.reader.ui.widget.WebView
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -34,27 +34,45 @@ import me.ash.reader.ui.widget.WebView
fun ReadPage( fun ReadPage(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ReadViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue() val viewState = readViewModel.viewState.collectAsStateValue()
val composition by rememberLottieComposition( var isScrollDown by remember { mutableStateOf(false) }
LottieCompositionSpec.Url(
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json"
)
)
LaunchedEffect(viewModel.viewState) { if (viewState.listState.isScrollInProgress) {
viewModel.viewState.collect { LaunchedEffect(Unit) {
Log.i("RLog", "scroll: start")
}
val preItemIndex by remember { mutableStateOf(viewState.listState.firstVisibleItemIndex) }
val preScrollStartOffset by remember { mutableStateOf(viewState.listState.firstVisibleItemScrollOffset) }
val currentItemIndex = viewState.listState.firstVisibleItemIndex
val currentScrollStartOffset = viewState.listState.firstVisibleItemScrollOffset
isScrollDown = when {
currentItemIndex > preItemIndex -> true
currentItemIndex < preItemIndex -> false
else -> currentScrollStartOffset > preScrollStartOffset
}
DisposableEffect(Unit) {
onDispose {
Log.i("RLog", "scroll: end")
}
}
}
LaunchedEffect(readViewModel.viewState) {
readViewModel.viewState.collect {
if (it.articleWithFeed != null) { if (it.articleWithFeed != null) {
// if (it.articleWithFeed.article.isUnread) { if (it.articleWithFeed.article.isUnread) {
// viewModel.dispatch(ReadViewAction.MarkUnread(false)) readViewModel.dispatch(ReadViewAction.MarkUnread(false))
// } }
if (it.articleWithFeed.feed.isFullContent) { if (it.articleWithFeed.feed.isFullContent) {
viewModel.dispatch(ReadViewAction.RenderFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
} }
} }
} }
@ -62,8 +80,49 @@ fun ReadPage(
Scaffold( Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface), modifier = Modifier.background(MaterialTheme.colorScheme.surface),
topBar = { topBar = {},
content = {
Box(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
contentAlignment = Alignment.TopCenter
) {
TopBar(isScrollDown, homeViewModel, scope, readViewModel, viewState)
}
Content(viewState, viewState.articleWithFeed, context)
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
contentAlignment = Alignment.BottomCenter
) {
BottomBar(isScrollDown, viewState.articleWithFeed, readViewModel)
}
}
},
bottomBar = {}
)
}
@Composable
private fun TopBar(
isScrollDown: Boolean,
homeViewModel: HomeViewModel,
scope: CoroutineScope,
readViewModel: ReadViewModel,
viewState: ReadViewState
) {
AnimatedVisibility(
visible = !isScrollDown,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
SmallTopAppBar( SmallTopAppBar(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(onClick = {
@ -104,25 +163,64 @@ fun ReadPage(
} }
} }
) )
}
}
@Composable
private fun BottomBar(
isScrollDown: Boolean,
articleWithFeed: ArticleWithFeed?,
readViewModel: ReadViewModel
) {
articleWithFeed?.let {
AnimatedVisibility(
visible = !isScrollDown,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
ReadBar(
disabled = false,
isUnread = articleWithFeed.article.isUnread,
isStarred = articleWithFeed.article.isStarred,
isFullContent = articleWithFeed.feed.isFullContent,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
}, },
content = { starredOnClick = {
if (viewState.articleWithFeed == null) { readViewModel.dispatch(ReadViewAction.MarkStarred(it))
},
fullContentOnClick = { afterIsFullContent ->
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
},
)
}
}
}
@Composable
private fun Content(
viewState: ReadViewState,
articleWithFeed: ArticleWithFeed?,
context: Context
) {
Column {
if (articleWithFeed == null) {
Spacer(modifier = Modifier.height(64.dp))
LottieAnimation( LottieAnimation(
composition = composition, modifier = Modifier.padding(80.dp),
modifier = Modifier url = "https://assets8.lottiefiles.com/packages/lf20_jm7mv1ib.json",
.padding(50.dp)
.alpha(0.6f),
isPlaying = true,
restartOnPlay = true,
iterations = Int.MAX_VALUE
) )
} else { } else {
LazyColumn( LazyColumn(
state = viewState.listState, state = viewState.listState,
) { ) {
val article = viewState.articleWithFeed.article val article = articleWithFeed.article
val feed = viewState.articleWithFeed.feed val feed = articleWithFeed.feed
item {
Spacer(modifier = Modifier.height(64.dp))
}
item { item {
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
Column( Column(
@ -132,7 +230,6 @@ fun ReadPage(
Header(context, article, feed) Header(context, article, feed)
} }
} }
item { item {
Spacer(modifier = Modifier.height(22.dp)) Spacer(modifier = Modifier.height(22.dp))
Crossfade(targetState = viewState.content) { content -> Crossfade(targetState = viewState.content) { content ->
@ -142,8 +239,12 @@ fun ReadPage(
Spacer(modifier = Modifier.height(50.dp)) Spacer(modifier = Modifier.height(50.dp))
} }
} }
item {
Spacer(modifier = Modifier.height(64.dp))
Spacer(modifier = Modifier.height(64.dp))
} }
} }
} }
)
} }
}

View File

@ -14,7 +14,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.LocalLightThemeColors
@Composable @Composable
fun Banner( fun Banner(
@ -25,7 +24,7 @@ fun Banner(
action: (@Composable () -> Unit)? = null, action: (@Composable () -> Unit)? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val lightThemeColors = LocalLightThemeColors.current val lightThemeColors = MaterialTheme.colorScheme
val lightPrimaryContainer = lightThemeColors.primaryContainer val lightPrimaryContainer = lightThemeColors.primaryContainer
val lightOnSurface = lightThemeColors.onSurface val lightOnSurface = lightThemeColors.onSurface

View File

@ -0,0 +1,26 @@
package me.ash.reader.ui.widget
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
@Composable
fun LottieAnimation(
modifier: Modifier = Modifier,
url: String,
) {
val composition by rememberLottieComposition(
LottieCompositionSpec.Url(url)
)
LottieAnimation(
composition = composition,
modifier = modifier,
isPlaying = true,
restartOnPlay = true,
iterations = Int.MAX_VALUE,
)
}

View File

@ -13,7 +13,6 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.ui.extension.collectAsStateValue
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
@ -30,7 +29,6 @@ fun WebView(
val context = LocalContext.current val context = LocalContext.current
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
val backgroundColor = MaterialTheme.colorScheme.surface.toArgb() val backgroundColor = MaterialTheme.colorScheme.surface.toArgb()
val viewState = viewModel.viewState.collectAsStateValue()
val webViewClient = object : WebViewClient() { val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {

View File

@ -44,4 +44,10 @@
<string name="mark_as_unread">标记为未读</string> <string name="mark_as_unread">标记为未读</string>
<string name="mark_as_starred">标记为已加星标</string> <string name="mark_as_starred">标记为已加星标</string>
<string name="mark_as_unstar">标记为未加星标</string> <string name="mark_as_unstar">标记为未加星标</string>
<string name="mark_as_read_one_day">超过 1 天标记为已读</string>
<string name="mark_as_read_three_days">超过 3 天标记为已读</string>
<string name="mark_as_read_seven_days">超过 7 天标记为已读</string>
<string name="one_day">1 天</string>
<string name="three_days">3 天</string>
<string name="seven_days">7 天</string>
</resources> </resources>

View File

@ -44,4 +44,10 @@
<string name="mark_as_unread">Mark as Unread</string> <string name="mark_as_unread">Mark as Unread</string>
<string name="mark_as_starred">Mark as Starred</string> <string name="mark_as_starred">Mark as Starred</string>
<string name="mark_as_unstar">Mark as Unstar</string> <string name="mark_as_unstar">Mark as Unstar</string>
<string name="mark_as_read_one_day">Mark as Read More Than 1 Day</string>
<string name="mark_as_read_three_days">Mark as Read More Than 3 Days</string>
<string name="mark_as_read_seven_days">Mark as Read More Than 7 Days</string>
<string name="one_day">1 d</string>
<string name="three_days">3 d</string>
<string name="seven_days">7 d</string>
</resources> </resources>