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 {
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")

View File

@ -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(
"""

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 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()

View File

@ -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
)
)
)
},
)
}
)
}

View File

@ -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))

View File

@ -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(),

View File

@ -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
)
)
)
},
)
}
)
}

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
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,8 +80,49 @@ fun ReadPage(
Scaffold(
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(
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
title = {},
navigationIcon = {
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 = {
if (viewState.articleWithFeed == null) {
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(
composition = composition,
modifier = Modifier
.padding(50.dp)
.alpha(0.6f),
isPlaying = true,
restartOnPlay = true,
iterations = Int.MAX_VALUE
modifier = Modifier.padding(80.dp),
url = "https://assets8.lottiefiles.com/packages/lf20_jm7mv1ib.json",
)
} else {
LazyColumn(
state = viewState.listState,
) {
val article = viewState.articleWithFeed.article
val feed = viewState.articleWithFeed.feed
val article = articleWithFeed.article
val feed = articleWithFeed.feed
item {
Spacer(modifier = Modifier.height(64.dp))
}
item {
Spacer(modifier = Modifier.height(2.dp))
Column(
@ -132,7 +230,6 @@ fun ReadPage(
Header(context, article, feed)
}
}
item {
Spacer(modifier = Modifier.height(22.dp))
Crossfade(targetState = viewState.content) { content ->
@ -142,8 +239,12 @@ fun ReadPage(
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.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

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.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? {

View File

@ -44,4 +44,10 @@
<string name="mark_as_unread">标记为未读</string>
<string name="mark_as_starred">标记为已加星标</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>

View File

@ -44,4 +44,10 @@
<string name="mark_as_unread">Mark as Unread</string>
<string name="mark_as_starred">Mark as Starred</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>