From 11d839fff415cd133cb936d651cc089b2822785e Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 29 Mar 2022 17:22:35 +0800 Subject: [PATCH] Min SDK upgraded to 26 and Full screen when scrolling down in the ReadPage --- app/build.gradle | 3 +- .../me/ash/reader/data/article/ArticleDao.kt | 37 ++ .../me/ash/reader/ui/page/home/FilterBar.kt | 58 +++ .../me/ash/reader/ui/page/home/FilterBar2.kt | 50 --- .../reader/ui/page/home/HomeBottomNavBar.kt | 409 ------------------ .../me/ash/reader/ui/page/home/HomePage.kt | 37 -- .../reader/ui/page/home/feeds/FeedsPage.kt | 38 +- .../home/feeds/subscribe/SubscribeDialog.kt | 43 +- .../reader/ui/page/home/flow/ArticleItem.kt | 3 +- .../ash/reader/ui/page/home/flow/FlowPage.kt | 25 +- .../reader/ui/page/home/flow/MarkAsReadBar.kt | 83 ++++ .../ash/reader/ui/page/home/read/ReadBar.kt | 139 ++++++ .../ash/reader/ui/page/home/read/ReadPage.kt | 299 ++++++++----- .../java/me/ash/reader/ui/widget/Banner.kt | 3 +- .../ash/reader/ui/widget/LottieAnimation.kt | 26 ++ .../java/me/ash/reader/ui/widget/WebView.kt | 2 - app/src/main/res/values-zh-rCN/strings.xml | 6 + app/src/main/res/values/strings.xml | 6 + 18 files changed, 631 insertions(+), 636 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt delete mode 100644 app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt delete mode 100644 app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt create mode 100644 app/src/main/java/me/ash/reader/ui/widget/LottieAnimation.kt diff --git a/app/build.gradle b/app/build.gradle index f8e1b83..a59b3b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { defaultConfig { applicationId "me.ash.reader" - minSdk 24 + minSdk 26 targetSdk 32 versionCode 1 versionName "1.0" @@ -50,6 +50,7 @@ android { } dependencies { + implementation("io.coil-kt:coil-compose:2.0.0-rc02") implementation("androidx.compose.animation:animation-graphics:$compose_version") implementation("com.google.accompanist:accompanist-flowlayout:0.24.3-alpha") implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha") diff --git a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt index ed43bc2..1a0eb44 100644 --- a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt @@ -6,6 +6,43 @@ import kotlinx.coroutines.flow.Flow @Dao 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( """ diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt new file mode 100644 index 0000000..13ceb10 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt @@ -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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt deleted file mode 100644 index 2487ee9..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt +++ /dev/null @@ -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)) - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt deleted file mode 100644 index 4616937..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt index 8f47ba0..4ca5b84 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt @@ -3,13 +3,10 @@ package me.ash.reader.ui.page.home import android.util.Log import androidx.activity.compose.BackHandler 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.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.google.accompanist.pager.ExperimentalPagerApi @@ -34,8 +31,6 @@ fun HomePage( feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), ) { val viewState = viewModel.viewState.collectAsStateValue() - val filterState = viewModel.filterState.collectAsStateValue() - val readState = readViewModel.viewState.collectAsStateValue() val scope = rememberCoroutineScope() 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() diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 1095860..10e987f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -3,10 +3,7 @@ package me.ash.reader.ui.page.home.feeds import androidx.compose.animation.Crossfade 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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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.getDesc 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.HomeViewModel 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.Subtitle -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, com.google.accompanist.pager.ExperimentalPagerApi::class) @Composable fun FeedsPage( modifier: Modifier = Modifier, navController: NavHostController, - viewModel: FeedsViewModel = hiltViewModel(), + feedsViewModel: FeedsViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(), subscribeViewModel: SubscribeViewModel = hiltViewModel(), ) { val scope = rememberCoroutineScope() - val viewState = viewModel.viewState.collectAsStateValue() + val viewState = feedsViewModel.viewState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue() val syncState = homeViewModel.syncState.collectAsStateValue() @@ -61,12 +59,12 @@ fun FeedsPage( ) LaunchedEffect(Unit) { - viewModel.dispatch(FeedsViewAction.FetchAccount) + feedsViewModel.dispatch(FeedsViewAction.FetchAccount) } LaunchedEffect(homeViewModel.filterState) { homeViewModel.filterState.collect { state -> - viewModel.dispatch( + feedsViewModel.dispatch( FeedsViewAction.FetchData(state) ) } @@ -114,7 +112,7 @@ fun FeedsPage( content = { SubscribeDialog( openInputStreamCallback = { - viewModel.dispatch(FeedsViewAction.AddFromFile(it)) + feedsViewModel.dispatch(FeedsViewAction.AddFromFile(it)) }, ) LazyColumn { @@ -213,9 +211,27 @@ fun FeedsPage( } } 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 + ) + ) + ) + }, + ) } ) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt index d5cfba5..c1c7eae 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt @@ -32,12 +32,16 @@ import java.io.InputStream @Composable fun SubscribeDialog( modifier: Modifier = Modifier, - viewModel: SubscribeViewModel = hiltViewModel(), + subscribeViewModel: SubscribeViewModel = hiltViewModel(), openInputStreamCallback: (InputStream) -> Unit, ) { val context = LocalContext.current val focusManager = LocalFocusManager.current 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()) { it?.let { uri -> 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 defaultString = stringResource(R.string.defaults) + LaunchedEffect(viewState.visible) { if (viewState.visible) { val defaultGroupId = context.dataStore .get(DataStoreKeys.CurrentAccountId)!! .spacerDollar(readYouString + defaultString) - viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId)) - viewModel.dispatch(SubscribeViewAction.Init) + subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId)) + subscribeViewModel.dispatch(SubscribeViewAction.Init) } else { - viewModel.dispatch(SubscribeViewAction.Reset) + subscribeViewModel.dispatch(SubscribeViewAction.Reset) viewState.pagerState.scrollToPage(0) } } @@ -80,7 +81,7 @@ fun SubscribeDialog( properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() - viewModel.dispatch(SubscribeViewAction.Hide) + subscribeViewModel.dispatch(SubscribeViewAction.Hide) }, icon = { Icon( @@ -102,10 +103,10 @@ fun SubscribeDialog( inputLink = viewState.linkContent, errorMessage = viewState.errorMessage, onLinkValueChange = { - viewModel.dispatch(SubscribeViewAction.InputLink(it)) + subscribeViewModel.dispatch(SubscribeViewAction.InputLink(it)) }, onSearchKeyboardAction = { - viewModel.dispatch(SubscribeViewAction.Search(scope)) + subscribeViewModel.dispatch(SubscribeViewAction.Search(scope)) }, link = viewState.linkContent, groups = groupsState.value, @@ -114,24 +115,24 @@ fun SubscribeDialog( selectedGroupId = viewState.selectedGroupId, newGroupContent = viewState.newGroupContent, onNewGroupValueChange = { - viewModel.dispatch(SubscribeViewAction.InputNewGroup(it)) + subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it)) }, newGroupSelected = viewState.newGroupSelected, changeNewGroupSelected = { - viewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it)) + subscribeViewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it)) }, pagerState = viewState.pagerState, allowNotificationPresetOnClick = { - viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) + subscribeViewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) }, parseFullContentPresetOnClick = { - viewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset) + subscribeViewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset) }, groupOnClick = { - viewModel.dispatch(SubscribeViewAction.SelectedGroup(it)) + subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(it)) }, onResultKeyboardAction = { - viewModel.dispatch(SubscribeViewAction.Subscribe) + subscribeViewModel.dispatch(SubscribeViewAction.Subscribe) } ) }, @@ -142,7 +143,7 @@ fun SubscribeDialog( enabled = viewState.linkContent.isNotEmpty(), onClick = { focusManager.clearFocus() - viewModel.dispatch(SubscribeViewAction.Search(scope)) + subscribeViewModel.dispatch(SubscribeViewAction.Search(scope)) } ) { Text( @@ -159,7 +160,7 @@ fun SubscribeDialog( TextButton( onClick = { focusManager.clearFocus() - viewModel.dispatch(SubscribeViewAction.Subscribe) + subscribeViewModel.dispatch(SubscribeViewAction.Subscribe) } ) { Text(stringResource(R.string.subscribe)) @@ -174,7 +175,7 @@ fun SubscribeDialog( onClick = { focusManager.clearFocus() launcher.launch("*/*") - viewModel.dispatch(SubscribeViewAction.Hide) + subscribeViewModel.dispatch(SubscribeViewAction.Hide) } ) { Text(text = stringResource(R.string.import_from_opml)) @@ -184,7 +185,7 @@ fun SubscribeDialog( TextButton( onClick = { focusManager.clearFocus() - viewModel.dispatch(SubscribeViewAction.Hide) + subscribeViewModel.dispatch(SubscribeViewAction.Hide) } ) { Text(text = stringResource(R.string.cancel)) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt index 9d5056d..a778ecc 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt @@ -25,13 +25,14 @@ fun ArticleItem( onClick: (ArticleWithFeed) -> Unit = {}, ) { val context = LocalContext.current + Column( modifier = Modifier .padding(horizontal = 12.dp) .clip(RoundedCornerShape(12.dp)) .clickable { onClick(articleWithFeed) } .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( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index c8c7251..6c9d0bc 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn 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.Search import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -26,6 +25,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import me.ash.reader.R import me.ash.reader.ui.extension.collectAsStateValue 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.HomeViewModel import me.ash.reader.ui.page.home.read.ReadViewModel @@ -47,6 +47,7 @@ fun FlowPage( val viewState = viewModel.viewState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue() val pagingItems = viewState.pagingData.collectAsLazyPagingItems() + var markAsRead by remember { mutableStateOf(false) } LaunchedEffect(homeViewModel.filterState) { homeViewModel.filterState.collect { state -> @@ -132,6 +133,24 @@ fun FlowPage( 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 + ) + ) + ) + }, + ) } ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt new file mode 100644 index 0000000..c680753 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt new file mode 100644 index 0000000..3424eda --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadBar.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt index 8f23571..c8bd3ed 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt @@ -1,6 +1,8 @@ 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.layout.* 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.rounded.Close 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.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.rememberLottieComposition +import kotlinx.coroutines.CoroutineScope import me.ash.reader.R +import me.ash.reader.data.article.ArticleWithFeed 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.widget.LottieAnimation import me.ash.reader.ui.widget.WebView @OptIn(ExperimentalMaterial3Api::class) @@ -34,27 +34,45 @@ import me.ash.reader.ui.widget.WebView fun ReadPage( navController: NavHostController, modifier: Modifier = Modifier, - viewModel: ReadViewModel = hiltViewModel(), - homeViewModel: HomeViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(), + homeViewModel: HomeViewModel = hiltViewModel(), ) { val context = LocalContext.current val scope = rememberCoroutineScope() - val viewState = viewModel.viewState.collectAsStateValue() - val composition by rememberLottieComposition( - LottieCompositionSpec.Url( - "https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json" - ) - ) + val viewState = readViewModel.viewState.collectAsStateValue() + var isScrollDown by remember { mutableStateOf(false) } - LaunchedEffect(viewModel.viewState) { - viewModel.viewState.collect { + if (viewState.listState.isScrollInProgress) { + 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.article.isUnread) { -// viewModel.dispatch(ReadViewAction.MarkUnread(false)) -// } + if (it.articleWithFeed.article.isUnread) { + readViewModel.dispatch(ReadViewAction.MarkUnread(false)) + } if (it.articleWithFeed.feed.isFullContent) { - viewModel.dispatch(ReadViewAction.RenderFullContent) + readViewModel.dispatch(ReadViewAction.RenderFullContent) } } } @@ -62,88 +80,171 @@ fun ReadPage( Scaffold( modifier = Modifier.background(MaterialTheme.colorScheme.surface), - topBar = { - SmallTopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = { - homeViewModel.dispatch( - HomeViewAction.ScrollToPage( - scope = scope, - targetPage = 1, - callback = { - readViewModel.dispatch(ReadViewAction.ClearArticle) - } - ) - ) - }) { - Icon( - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, - actions = { - viewState.articleWithFeed?.let { - IconButton(onClick = {}) { - Icon( - modifier = Modifier.size(22.dp), - imageVector = Icons.Outlined.Headphones, - contentDescription = stringResource(R.string.mark_all_as_read), - tint = MaterialTheme.colorScheme.onSurface, - ) - } - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = stringResource(R.string.search), - tint = MaterialTheme.colorScheme.onSurface, - ) - } - } - } - ) - }, + topBar = {}, content = { - if (viewState.articleWithFeed == null) { - LottieAnimation( - composition = composition, + Box(Modifier.fillMaxSize()) { + Box( modifier = Modifier - .padding(50.dp) - .alpha(0.6f), - isPlaying = true, - restartOnPlay = true, - iterations = Int.MAX_VALUE - ) - } else { - LazyColumn( - state = viewState.listState, + .fillMaxSize() + .zIndex(1f), + contentAlignment = Alignment.TopCenter ) { - val article = viewState.articleWithFeed.article - val feed = viewState.articleWithFeed.feed + 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 = {} + ) +} - item { - Spacer(modifier = Modifier.height(2.dp)) - Column( - modifier = Modifier - .padding(horizontal = 12.dp) - ) { - Header(context, article, feed) - } +@Composable +private fun TopBar( + isScrollDown: Boolean, + homeViewModel: HomeViewModel, + scope: CoroutineScope, + readViewModel: ReadViewModel, + viewState: ReadViewState +) { + AnimatedVisibility( + visible = !isScrollDown, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + SmallTopAppBar( + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + title = {}, + navigationIcon = { + IconButton(onClick = { + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + callback = { + readViewModel.dispatch(ReadViewAction.ClearArticle) + } + ) + ) + }) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, + actions = { + viewState.articleWithFeed?.let { + IconButton(onClick = {}) { + Icon( + modifier = Modifier.size(22.dp), + imageVector = Icons.Outlined.Headphones, + contentDescription = stringResource(R.string.mark_all_as_read), + tint = MaterialTheme.colorScheme.onSurface, + ) } - - item { - Spacer(modifier = Modifier.height(22.dp)) - Crossfade(targetState = viewState.content) { content -> - WebView( - content = content ?: "", - ) - Spacer(modifier = Modifier.height(50.dp)) - } + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.search), + tint = MaterialTheme.colorScheme.onSurface, + ) } } } - } - ) + ) + } } + +@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)) + }, + starredOnClick = { + 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( + modifier = Modifier.padding(80.dp), + url = "https://assets8.lottiefiles.com/packages/lf20_jm7mv1ib.json", + ) + } else { + LazyColumn( + state = viewState.listState, + ) { + val article = articleWithFeed.article + val feed = articleWithFeed.feed + + item { + Spacer(modifier = Modifier.height(64.dp)) + } + item { + Spacer(modifier = Modifier.height(2.dp)) + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + ) { + Header(context, article, feed) + } + } + item { + Spacer(modifier = Modifier.height(22.dp)) + Crossfade(targetState = viewState.content) { content -> + WebView( + content = content ?: "", + ) + Spacer(modifier = Modifier.height(50.dp)) + } + } + item { + Spacer(modifier = Modifier.height(64.dp)) + Spacer(modifier = Modifier.height(64.dp)) + } + } + } + } +} + diff --git a/app/src/main/java/me/ash/reader/ui/widget/Banner.kt b/app/src/main/java/me/ash/reader/ui/widget/Banner.kt index ef3e81f..9b0c904 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/Banner.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/Banner.kt @@ -14,7 +14,6 @@ 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 -import me.ash.reader.ui.theme.LocalLightThemeColors @Composable fun Banner( @@ -25,7 +24,7 @@ fun Banner( action: (@Composable () -> Unit)? = null, onClick: () -> Unit = {}, ) { - val lightThemeColors = LocalLightThemeColors.current + val lightThemeColors = MaterialTheme.colorScheme val lightPrimaryContainer = lightThemeColors.primaryContainer val lightOnSurface = lightThemeColors.onSurface diff --git a/app/src/main/java/me/ash/reader/ui/widget/LottieAnimation.kt b/app/src/main/java/me/ash/reader/ui/widget/LottieAnimation.kt new file mode 100644 index 0000000..eab95f0 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/widget/LottieAnimation.kt @@ -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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/widget/WebView.kt b/app/src/main/java/me/ash/reader/ui/widget/WebView.kt index 4f928f3..a694ebb 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/WebView.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/WebView.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView 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.ReadViewModel @@ -30,7 +29,6 @@ fun WebView( val context = LocalContext.current val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() val backgroundColor = MaterialTheme.colorScheme.surface.toArgb() - val viewState = viewModel.viewState.collectAsStateValue() val webViewClient = object : WebViewClient() { override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 79754ec..e758fc0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -44,4 +44,10 @@ 标记为未读 标记为已加星标 标记为未加星标 + 超过 1 天标记为已读 + 超过 3 天标记为已读 + 超过 7 天标记为已读 + 1 天 + 3 天 + 7 天 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bfedd6a..186cea3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,4 +44,10 @@ Mark as Unread Mark as Starred Mark as Unstar + Mark as Read More Than 1 Day + Mark as Read More Than 3 Days + Mark as Read More Than 7 Days + 1 d + 3 d + 7 d \ No newline at end of file