Add mark all as read feature
This commit is contained in:
parent
f95108fa67
commit
aaf032332b
|
@ -6,46 +6,68 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import me.ash.reader.data.entity.Article
|
import me.ash.reader.data.entity.Article
|
||||||
import me.ash.reader.data.entity.ArticleWithFeed
|
import me.ash.reader.data.entity.ArticleWithFeed
|
||||||
import me.ash.reader.data.entity.ImportantCount
|
import me.ash.reader.data.entity.ImportantCount
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ArticleDao {
|
interface ArticleDao {
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
UPDATE article SET isUnread = 0
|
UPDATE article SET isUnread = :isUnread
|
||||||
WHERE accountId = :accountId
|
WHERE accountId = :accountId
|
||||||
AND isUnread = 1
|
AND date < :before
|
||||||
AND date <= :before
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
suspend fun markAllAsRead(accountId: Int, before: Long)
|
suspend fun markAllAsRead(
|
||||||
|
accountId: Int,
|
||||||
|
isUnread: Boolean,
|
||||||
|
before: Date,
|
||||||
|
)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
UPDATE article SET isUnread = 0
|
UPDATE article SET isUnread = :isUnread
|
||||||
WHERE accountId = :accountId
|
WHERE feedId IN (
|
||||||
AND isUnread = 1
|
SELECT id FROM feed
|
||||||
AND date <= :before
|
WHERE groupId = :groupId
|
||||||
AND feedId = :feedId
|
)
|
||||||
|
AND accountId = :accountId
|
||||||
|
AND date < :before
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
suspend fun markAllAsReadByFeedId(accountId: Int, before: Long, feedId: String)
|
suspend fun markAllAsReadByGroupId(
|
||||||
//
|
accountId: Int,
|
||||||
// @Query(
|
groupId: String,
|
||||||
// """
|
isUnread: Boolean,
|
||||||
// UPDATE article SET isUnread = 0
|
before: Date,
|
||||||
// WHERE accountId = :accountId
|
)
|
||||||
// AND isUnread = 1
|
|
||||||
// AND date <= :before
|
@Query(
|
||||||
// AND feedId = :feedId
|
"""
|
||||||
//
|
UPDATE article SET isUnread = :isUnread
|
||||||
// SELECT * FROM `group` AS a, feed AS b, article AS c
|
WHERE feedId = :feedId
|
||||||
// WHERE a.accountId = :accountId
|
AND accountId = :accountId
|
||||||
// AND a.id = b.groupId
|
AND date < :before
|
||||||
// AND b.groupId = :groupId
|
"""
|
||||||
// AND c.feedId = b.id
|
)
|
||||||
// """
|
suspend fun markAllAsReadByFeedId(
|
||||||
// )
|
accountId: Int,
|
||||||
// suspend fun markAllAsReadByGroupId(accountId: Int, before: Long, groupId: String)
|
feedId: String,
|
||||||
|
isUnread: Boolean,
|
||||||
|
before: Date,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
UPDATE article SET isUnread = :isUnread
|
||||||
|
WHERE id = :articleId
|
||||||
|
AND accountId = :accountId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun markAsReadByArticleId(
|
||||||
|
accountId: Int,
|
||||||
|
articleId: String,
|
||||||
|
isUnread: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -38,6 +38,14 @@ abstract class AbstractRssRepository constructor(
|
||||||
|
|
||||||
abstract suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result
|
abstract suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result
|
||||||
|
|
||||||
|
abstract suspend fun markAsRead(
|
||||||
|
groupId: String?,
|
||||||
|
feedId: String?,
|
||||||
|
articleId: String?,
|
||||||
|
before: Date?,
|
||||||
|
isUnread: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
fun doSync() {
|
fun doSync() {
|
||||||
workManager.enqueueUniquePeriodicWork(
|
workManager.enqueueUniquePeriodicWork(
|
||||||
SyncWorker.WORK_NAME,
|
SyncWorker.WORK_NAME,
|
||||||
|
|
|
@ -125,6 +125,40 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun markAsRead(
|
||||||
|
groupId: String?,
|
||||||
|
feedId: String?,
|
||||||
|
articleId: String?,
|
||||||
|
before: Date?,
|
||||||
|
isUnread: Boolean,
|
||||||
|
) {
|
||||||
|
val accountId = context.currentAccountId
|
||||||
|
when {
|
||||||
|
groupId != null -> {
|
||||||
|
articleDao.markAllAsReadByGroupId(
|
||||||
|
accountId = accountId,
|
||||||
|
groupId = groupId,
|
||||||
|
isUnread = isUnread,
|
||||||
|
before = before ?: Date(Long.MAX_VALUE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
feedId != null -> {
|
||||||
|
articleDao.markAllAsReadByFeedId(
|
||||||
|
accountId = accountId,
|
||||||
|
feedId = feedId,
|
||||||
|
isUnread = isUnread,
|
||||||
|
before = before ?: Date(Long.MAX_VALUE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
articleId != null -> {
|
||||||
|
articleDao.markAsReadByArticleId(accountId, articleId, isUnread)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
articleDao.markAllAsRead(accountId, isUnread, before ?: Date(Long.MAX_VALUE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ArticleNotify(
|
data class ArticleNotify(
|
||||||
val articles: List<Article>,
|
val articles: List<Article>,
|
||||||
val isNotify: Boolean,
|
val isNotify: Boolean,
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package me.ash.reader.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import androidx.compose.ui.window.Popup
|
||||||
|
import androidx.compose.ui.window.PopupPositionProvider
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
|
import com.google.accompanist.insets.LocalWindowInsets
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedPopup(
|
||||||
|
visible: Boolean = false,
|
||||||
|
absoluteY: Dp = Dp.Hairline,
|
||||||
|
absoluteX: Dp = Dp.Hairline,
|
||||||
|
onDismissRequest: () -> Unit = {},
|
||||||
|
content: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val insets = LocalWindowInsets.current
|
||||||
|
|
||||||
|
Popup(
|
||||||
|
properties = PopupProperties(focusable = visible),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
popupPositionProvider = object : PopupPositionProvider {
|
||||||
|
override fun calculatePosition(
|
||||||
|
anchorBounds: IntRect,
|
||||||
|
windowSize: IntSize,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
popupContentSize: IntSize
|
||||||
|
): IntOffset {
|
||||||
|
return IntOffset(
|
||||||
|
x = with(density) { (absoluteX).roundToPx() },
|
||||||
|
y = with(density) { (absoluteY).roundToPx() + insets.statusBars.top }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically(),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,13 @@ package me.ash.reader.ui.component
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.BaselineShift
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@ -28,8 +30,11 @@ fun DisplayText(
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.height(44.dp),
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
baselineShift = BaselineShift.Superscript
|
||||||
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -40,8 +45,11 @@ fun DisplayText(
|
||||||
exit = fadeOut() + shrinkVertically(),
|
exit = fadeOut() + shrinkVertically(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.height(16.dp),
|
||||||
text = desc,
|
text = desc,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium.copy(
|
||||||
|
baselineShift = BaselineShift.Superscript
|
||||||
|
),
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|
|
@ -70,28 +70,30 @@ fun Modifier.combinedFeedbackClickable(
|
||||||
isSound: Boolean? = false,
|
isSound: Boolean? = false,
|
||||||
onPressDown: (() -> Unit)? = null,
|
onPressDown: (() -> Unit)? = null,
|
||||||
onPressUp: (() -> Unit)? = null,
|
onPressUp: (() -> Unit)? = null,
|
||||||
|
onTap: (() -> Unit)? = null,
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
onDoubleClick: (() -> Unit)? = null,
|
onDoubleClick: (() -> Unit)? = null,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
): Modifier {
|
): Modifier {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
return if (onPressDown != null || onPressUp != null) {
|
return if (onPressDown != null || onPressUp != null || onTap != null) {
|
||||||
indication(interactionSource, LocalIndication.current)
|
indication(interactionSource, LocalIndication.current)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = { offset ->
|
onPress = { offset ->
|
||||||
onPressDown?.let {
|
onPressDown?.let {
|
||||||
|
it()
|
||||||
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
val press = PressInteraction.Press(offset)
|
val press = PressInteraction.Press(offset)
|
||||||
interactionSource.emit(press)
|
interactionSource.emit(press)
|
||||||
tryAwaitRelease()
|
tryAwaitRelease()
|
||||||
|
onPressUp?.invoke()
|
||||||
interactionSource.emit(PressInteraction.Release(press))
|
interactionSource.emit(PressInteraction.Release(press))
|
||||||
it()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
onPressUp?.let {
|
onTap?.let {
|
||||||
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
|
if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
it()
|
it()
|
||||||
|
|
|
@ -64,7 +64,7 @@ fun FeedItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.outline),
|
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||||
) {}
|
) {}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
|
||||||
|
|
|
@ -109,7 +109,6 @@ fun FeedsPage(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.Outlined.Settings,
|
imageVector = Icons.Outlined.Settings,
|
||||||
contentDescription = stringResource(R.string.settings),
|
contentDescription = stringResource(R.string.settings),
|
||||||
|
@ -120,7 +119,6 @@ fun FeedsPage(
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
|
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
|
||||||
imageVector = Icons.Rounded.Refresh,
|
imageVector = Icons.Rounded.Refresh,
|
||||||
contentDescription = stringResource(R.string.refresh),
|
contentDescription = stringResource(R.string.refresh),
|
||||||
|
@ -131,7 +129,6 @@ fun FeedsPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
contentDescription = stringResource(R.string.subscribe),
|
contentDescription = stringResource(R.string.subscribe),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|
|
@ -3,8 +3,10 @@ package me.ash.reader.ui.page.home.flow
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.*
|
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.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
@ -16,7 +18,6 @@ import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -71,28 +72,13 @@ fun FlowPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LaunchedEffect(viewState.listState.isScrollInProgress) {
|
|
||||||
// Log.i("RLog", "isScrollInProgress: ${viewState.listState.isScrollInProgress}")
|
|
||||||
// if (viewState.listState.isScrollInProgress) {
|
|
||||||
// Log.i("RLog", "isScrollInProgress: ${true}")
|
|
||||||
// markAsRead = false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.pointerInput(markAsRead) {
|
|
||||||
detectTapGestures {
|
|
||||||
markAsRead = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
topBar = {
|
topBar = {
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Rounded.ArrowBack,
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back),
|
contentDescription = stringResource(R.string.back),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
@ -107,7 +93,6 @@ fun FlowPage(
|
||||||
exit = fadeOut() + shrinkVertically(),
|
exit = fadeOut() + shrinkVertically(),
|
||||||
) {
|
) {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Rounded.DoneAll,
|
imageVector = Icons.Rounded.DoneAll,
|
||||||
contentDescription = stringResource(R.string.mark_all_as_read),
|
contentDescription = stringResource(R.string.mark_all_as_read),
|
||||||
tint = if (markAsRead) {
|
tint = if (markAsRead) {
|
||||||
|
@ -123,7 +108,6 @@ fun FlowPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Rounded.Search,
|
imageVector = Icons.Rounded.Search,
|
||||||
contentDescription = stringResource(R.string.search),
|
contentDescription = stringResource(R.string.search),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
@ -162,17 +146,30 @@ fun FlowPage(
|
||||||
enter = fadeIn() + expandVertically(),
|
enter = fadeIn() + expandVertically(),
|
||||||
exit = fadeOut() + shrinkVertically(),
|
exit = fadeOut() + shrinkVertically(),
|
||||||
) {
|
) {
|
||||||
Column {
|
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
|
||||||
MarkAsReadBar()
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
}
|
}
|
||||||
|
MarkAsReadBar(
|
||||||
|
visible = markAsRead,
|
||||||
|
absoluteY = if (isSyncing) (4 + 16 + 180).dp else 180.dp,
|
||||||
|
onDismissRequest = {
|
||||||
|
markAsRead = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
markAsRead = false
|
||||||
|
flowViewModel.dispatch(
|
||||||
|
FlowViewAction.MarkAsRead(
|
||||||
|
groupId = filterState.group?.id,
|
||||||
|
feedId = filterState.feed?.id,
|
||||||
|
articleId = null,
|
||||||
|
markAsReadBefore = it,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
generateArticleList(
|
generateArticleList(
|
||||||
context = context,
|
context = context,
|
||||||
pagingItems = pagingItems,
|
pagingItems = pagingItems,
|
||||||
) {
|
) {
|
||||||
markAsRead = false
|
|
||||||
onItemClick(it)
|
onItemClick(it)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
@ -190,14 +187,7 @@ fun FlowPage(
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
filter = filterState.filter,
|
filter = filterState.filter,
|
||||||
filterOnClick = {
|
filterOnClick = { onFilterChange(filterState.copy(filter = it)) },
|
||||||
markAsRead = false
|
|
||||||
onFilterChange(
|
|
||||||
filterState.copy(
|
|
||||||
filter = it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.entity.ArticleWithFeed
|
import me.ash.reader.data.entity.ArticleWithFeed
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import me.ash.reader.ui.page.home.FilterState
|
import me.ash.reader.ui.page.home.FilterState
|
||||||
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
@ -28,14 +29,11 @@ class FlowViewModel @Inject constructor(
|
||||||
is FlowViewAction.FetchData -> fetchData(action.filterState)
|
is FlowViewAction.FetchData -> fetchData(action.filterState)
|
||||||
is FlowViewAction.ChangeRefreshing -> changeRefreshing(action.isRefreshing)
|
is FlowViewAction.ChangeRefreshing -> changeRefreshing(action.isRefreshing)
|
||||||
is FlowViewAction.ScrollToItem -> scrollToItem(action.index)
|
is FlowViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||||
is FlowViewAction.PeekSyncWork -> peekSyncWork()
|
is FlowViewAction.MarkAsRead -> markAsRead(
|
||||||
}
|
action.groupId,
|
||||||
}
|
action.feedId,
|
||||||
|
action.articleId,
|
||||||
private fun peekSyncWork() {
|
action.markAsReadBefore,
|
||||||
_viewState.update {
|
|
||||||
it.copy(
|
|
||||||
syncWorkInfo = rssRepository.get().peekWork()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +74,37 @@ class FlowViewModel @Inject constructor(
|
||||||
it.copy(isRefreshing = isRefreshing)
|
it.copy(isRefreshing = isRefreshing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun markAsRead(
|
||||||
|
groupId: String?,
|
||||||
|
feedId: String?,
|
||||||
|
articleId: String?,
|
||||||
|
markAsReadBefore: MarkAsReadBefore
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
rssRepository.get().markAsRead(
|
||||||
|
groupId = groupId,
|
||||||
|
feedId = feedId,
|
||||||
|
articleId = articleId,
|
||||||
|
before = when (markAsReadBefore) {
|
||||||
|
MarkAsReadBefore.All -> null
|
||||||
|
MarkAsReadBefore.OneDay -> Calendar.getInstance().apply {
|
||||||
|
time = Date()
|
||||||
|
add(Calendar.DAY_OF_MONTH, -1)
|
||||||
|
}.time
|
||||||
|
MarkAsReadBefore.ThreeDays -> Calendar.getInstance().apply {
|
||||||
|
time = Date()
|
||||||
|
add(Calendar.DAY_OF_MONTH, -3)
|
||||||
|
}.time
|
||||||
|
MarkAsReadBefore.SevenDays -> Calendar.getInstance().apply {
|
||||||
|
time = Date()
|
||||||
|
add(Calendar.DAY_OF_MONTH, -7)
|
||||||
|
}.time
|
||||||
|
},
|
||||||
|
isUnread = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ArticleViewState(
|
data class ArticleViewState(
|
||||||
|
@ -99,5 +128,17 @@ sealed class FlowViewAction {
|
||||||
val index: Int
|
val index: Int
|
||||||
) : FlowViewAction()
|
) : FlowViewAction()
|
||||||
|
|
||||||
object PeekSyncWork : FlowViewAction()
|
data class MarkAsRead(
|
||||||
|
val groupId: String?,
|
||||||
|
val feedId: String?,
|
||||||
|
val articleId: String?,
|
||||||
|
val markAsReadBefore: MarkAsReadBefore
|
||||||
|
) : FlowViewAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class MarkAsReadBefore {
|
||||||
|
SevenDays,
|
||||||
|
ThreeDays,
|
||||||
|
OneDay,
|
||||||
|
All,
|
||||||
}
|
}
|
|
@ -1,5 +1,10 @@
|
||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.SoundEffectConstants
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
@ -7,15 +12,36 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.component.AnimatedPopup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkAsReadBar() {
|
fun MarkAsReadBar(
|
||||||
|
visible: Boolean = false,
|
||||||
|
absoluteY: Dp = Dp.Hairline,
|
||||||
|
onDismissRequest: () -> Unit = {},
|
||||||
|
onItemClick: (MarkAsReadBefore) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val animated = remember { Animatable(absoluteY.value) }
|
||||||
|
|
||||||
|
LaunchedEffect(absoluteY) {
|
||||||
|
animated.animateTo(absoluteY.value, spring(stiffness = Spring.StiffnessMediumLow))
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedPopup(
|
||||||
|
visible = visible,
|
||||||
|
absoluteY = animated.value.dp,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
|
@ -23,22 +49,31 @@ fun MarkAsReadBar() {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
MarkAsReadBarItem(
|
MarkAsReadBarItem(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.width(56.dp),
|
||||||
text = stringResource(R.string.seven_days),
|
text = stringResource(R.string.seven_days),
|
||||||
)
|
) {
|
||||||
|
onItemClick(MarkAsReadBefore.SevenDays)
|
||||||
|
}
|
||||||
MarkAsReadBarItem(
|
MarkAsReadBarItem(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.width(56.dp),
|
||||||
text = stringResource(R.string.three_days),
|
text = stringResource(R.string.three_days),
|
||||||
)
|
) {
|
||||||
|
onItemClick(MarkAsReadBefore.ThreeDays)
|
||||||
|
}
|
||||||
|
MarkAsReadBarItem(
|
||||||
|
modifier = Modifier.width(56.dp),
|
||||||
|
text = stringResource(R.string.one_day),
|
||||||
|
) {
|
||||||
|
onItemClick(MarkAsReadBefore.OneDay)
|
||||||
|
}
|
||||||
MarkAsReadBarItem(
|
MarkAsReadBarItem(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
text = stringResource(R.string.one_day),
|
|
||||||
)
|
|
||||||
MarkAsReadBarItem(
|
|
||||||
modifier = Modifier.weight(2.5f),
|
|
||||||
text = stringResource(R.string.mark_all_as_read),
|
text = stringResource(R.string.mark_all_as_read),
|
||||||
isPrimary = true,
|
isPrimary = true,
|
||||||
)
|
) {
|
||||||
|
onItemClick(MarkAsReadBefore.All)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,12 +82,19 @@ fun MarkAsReadBarItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String,
|
||||||
isPrimary: Boolean = false,
|
isPrimary: Boolean = false,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(52.dp)
|
.height(56.dp)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.clickable { },
|
.clickable {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = if (isPrimary) {
|
color = if (isPrimary) {
|
||||||
|
@ -70,7 +112,7 @@ fun MarkAsReadBarItem(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
color = if (isPrimary) {
|
color = if (isPrimary) {
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onSurface
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.secondary
|
MaterialTheme.colorScheme.secondary
|
||||||
},
|
},
|
||||||
|
|
|
@ -143,7 +143,6 @@ private fun TopBar(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Rounded.Close,
|
imageVector = Icons.Rounded.Close,
|
||||||
contentDescription = stringResource(R.string.close),
|
contentDescription = stringResource(R.string.close),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
@ -156,7 +155,6 @@ private fun TopBar(
|
||||||
actions = {
|
actions = {
|
||||||
if (isShowActions) {
|
if (isShowActions) {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(22.dp),
|
||||||
imageVector = Icons.Outlined.Headphones,
|
imageVector = Icons.Outlined.Headphones,
|
||||||
contentDescription = stringResource(R.string.mark_all_as_read),
|
contentDescription = stringResource(R.string.mark_all_as_read),
|
||||||
|
@ -164,7 +162,6 @@ private fun TopBar(
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Outlined.MoreVert,
|
imageVector = Icons.Outlined.MoreVert,
|
||||||
contentDescription = stringResource(R.string.search),
|
contentDescription = stringResource(R.string.search),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|
|
@ -79,7 +79,7 @@ class ReadViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun markUnread(isUnread: Boolean) {
|
private fun markUnread(isUnread: Boolean) {
|
||||||
val articleWithFeed = _viewState.value.articleWithFeed ?: return
|
val articleWithFeed = _viewState.value.articleWithFeed ?: return
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
articleWithFeed = articleWithFeed.copy(
|
articleWithFeed = articleWithFeed.copy(
|
||||||
|
@ -89,10 +89,12 @@ class ReadViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rssRepository.get().updateArticleInfo(
|
rssRepository.get().markAsRead(
|
||||||
articleWithFeed.article.copy(
|
groupId = null,
|
||||||
isUnread = isUnread
|
feedId = null,
|
||||||
)
|
articleId = _viewState.value.articleWithFeed!!.article.id,
|
||||||
|
before = null,
|
||||||
|
isUnread = isUnread,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ fun SettingsPage(
|
||||||
title = {},
|
title = {},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
FeedbackIconButton(
|
FeedbackIconButton(
|
||||||
isHaptic = false,
|
|
||||||
imageVector = Icons.Rounded.ArrowBack,
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
contentDescription = stringResource(R.string.back),
|
contentDescription = stringResource(R.string.back),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
Loading…
Reference in New Issue
Block a user