Improve SyncState

This commit is contained in:
Ash 2022-04-03 18:15:28 +08:00
parent 54506e5019
commit 5f11616c6a
12 changed files with 296 additions and 232 deletions

View File

@ -48,8 +48,8 @@ class App : Application(), Configuration.Provider {
@Inject
lateinit var localRssRepository: LocalRssRepository
@Inject
lateinit var feverRssRepository: FeverRssRepository
// @Inject
// lateinit var feverRssRepository: FeverRssRepository
@Inject
lateinit var opmlRepository: OpmlRepository

View File

@ -8,8 +8,8 @@ import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
@ -17,6 +17,7 @@ import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.*
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.currentAccountId
import java.util.*
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor(
@ -29,22 +30,13 @@ abstract class AbstractRssRepository constructor(
private val workManager: WorkManager,
private val dispatcherIO: CoroutineDispatcher,
) {
data class SyncState(
val feedCount: Int = 0,
val syncedCount: Int = 0,
val currentFeedName: String = "",
) {
val isSyncing: Boolean = feedCount != 0 || syncedCount != 0 || currentFeedName != ""
val isNotSyncing: Boolean = !isSyncing
}
abstract suspend fun updateArticleInfo(article: Article)
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
abstract suspend fun addGroup(name: String): String
abstract suspend fun sync()
abstract suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result
fun doSync() {
workManager.enqueueUniquePeriodicWork(
@ -148,17 +140,6 @@ abstract class AbstractRssRepository constructor(
articleDao.deleteByFeedId(context.currentAccountId, feed.id)
feedDao.delete(feed)
}
companion object {
val mutex = Mutex()
private val _syncState = MutableStateFlow(SyncState())
val syncState = _syncState.asStateFlow()
fun updateSyncState(function: (SyncState) -> SyncState) {
_syncState.update(function)
}
}
}
@HiltWorker
@ -170,18 +151,24 @@ class SyncWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
rssRepository.get().sync()
return Result.success()
return rssRepository.get().sync(this)
}
companion object {
const val WORK_NAME = "article.sync"
val UUID: UUID
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.build()
).addTag(WORK_NAME).build()
).addTag(WORK_NAME).build().also {
UUID = it.id
}
fun setIsSyncing(boolean: Boolean) = workDataOf("isSyncing" to boolean)
fun Data.getIsSyncing(): Boolean = getBoolean("isSyncing", false)
}
}

View File

@ -1,163 +1,163 @@
package me.ash.reader.data.repository
import android.content.Context
import android.util.Log
import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.spacerDollar
import net.dankito.readability4j.extended.Readability4JExtended
import java.util.*
import javax.inject.Inject
import kotlin.collections.set
class FeverRssRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val articleDao: ArticleDao,
private val feedDao: FeedDao,
private val groupDao: GroupDao,
private val rssHelper: RssHelper,
private val feverApiDataSource: FeverApiDataSource,
private val accountDao: AccountDao,
rssNetworkDataSource: RssNetworkDataSource,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
workManager: WorkManager,
) : AbstractRssRepository(
context, accountDao, articleDao, groupDao,
feedDao, rssNetworkDataSource, workManager,
dispatcherIO
) {
override suspend fun updateArticleInfo(article: Article) {
articleDao.update(article)
}
override suspend fun subscribe(feed: Feed, articles: List<Article>) {
feedDao.insert(feed)
articleDao.insertList(articles.map {
it.copy(feedId = feed.id)
})
}
override suspend fun addGroup(name: String): String {
return UUID.randomUUID().toString().also {
groupDao.insert(
Group(
id = it,
name = name,
accountId = context.currentAccountId
)
)
}
}
override suspend fun sync() {
applicationScope.launch(dispatcherDefault) {
mutex.withLock {
val accountId = context.currentAccountId
updateSyncState {
it.copy(
feedCount = 1,
syncedCount = 1,
currentFeedName = "Fever"
)
}
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
// Temporary add feeds
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
Log.i("RLog", "Fever groups: $feverGroupsBody")
feverGroupsBody.groups.forEach {
groupDao.insert(
Group(
id = accountId.spacerDollar(it.id),
name = it.title,
accountId = accountId,
)
)
}
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
feverGroupsBody.feeds_groups.forEach { item ->
item.feed_ids
.split(",")
.map { it.toInt() }
.forEach { id ->
feverFeedsGroupsMap[id] = item.group_id
}
}
val feeds = feverFeeds.map {
Feed(
id = accountId.spacerDollar(it.id),
name = it.title,
url = it.url,
groupId = feverFeedsGroupsMap[it.id].toString(),
accountId = accountId
)
}
feedDao.insertList(feeds)
}
// Add articles
val articles = mutableListOf<Article>()
feverApiDataSource.itemsBySince(since = 1647444325925621L)
.execute().body()!!.items
.forEach {
articles.add(
Article(
id = accountId.spacerDollar(it.id),
date = Date(it.created_on_time * 1000),
title = it.title,
author = it.author,
rawDescription = it.html,
shortDescription = (
Readability4JExtended("", it.html)
.parse().textContent ?: ""
).take(100).trim(),
link = it.url,
accountId = accountId,
feedId = it.feed_id.toString(),
isUnread = it.is_read == 0,
isStarred = it.is_saved == 1,
)
)
}
articleDao.insertList(articles)
// Complete sync
accountDao.update(accountDao.queryById(accountId)!!.apply {
updateAt = Date()
})
updateSyncState {
it.copy(
feedCount = 0,
syncedCount = 0,
currentFeedName = ""
)
}
}
}
}
}
//package me.ash.reader.data.repository
//
//import android.content.Context
//import android.util.Log
//import androidx.work.WorkManager
//import dagger.hilt.android.qualifiers.ApplicationContext
//import kotlinx.coroutines.CoroutineDispatcher
//import kotlinx.coroutines.CoroutineScope
//import kotlinx.coroutines.launch
//import kotlinx.coroutines.sync.withLock
//import me.ash.reader.data.dao.AccountDao
//import me.ash.reader.data.dao.ArticleDao
//import me.ash.reader.data.dao.FeedDao
//import me.ash.reader.data.dao.GroupDao
//import me.ash.reader.data.entity.Article
//import me.ash.reader.data.entity.Feed
//import me.ash.reader.data.entity.Group
//import me.ash.reader.data.module.ApplicationScope
//import me.ash.reader.data.module.DispatcherDefault
//import me.ash.reader.data.module.DispatcherIO
//import me.ash.reader.data.source.FeverApiDataSource
//import me.ash.reader.data.source.RssNetworkDataSource
//import me.ash.reader.ui.ext.currentAccountId
//import me.ash.reader.ui.ext.spacerDollar
//import net.dankito.readability4j.extended.Readability4JExtended
//import java.util.*
//import javax.inject.Inject
//import kotlin.collections.set
//
//class FeverRssRepository @Inject constructor(
// @ApplicationContext
// private val context: Context,
// private val articleDao: ArticleDao,
// private val feedDao: FeedDao,
// private val groupDao: GroupDao,
// private val rssHelper: RssHelper,
// private val feverApiDataSource: FeverApiDataSource,
// private val accountDao: AccountDao,
// rssNetworkDataSource: RssNetworkDataSource,
// @ApplicationScope
// private val applicationScope: CoroutineScope,
// @DispatcherDefault
// private val dispatcherDefault: CoroutineDispatcher,
// @DispatcherIO
// private val dispatcherIO: CoroutineDispatcher,
// workManager: WorkManager,
//) : AbstractRssRepository(
// context, accountDao, articleDao, groupDao,
// feedDao, rssNetworkDataSource, workManager,
// dispatcherIO
//) {
// override suspend fun updateArticleInfo(article: Article) {
// articleDao.update(article)
// }
//
// override suspend fun subscribe(feed: Feed, articles: List<Article>) {
// feedDao.insert(feed)
// articleDao.insertList(articles.map {
// it.copy(feedId = feed.id)
// })
// }
//
// override suspend fun addGroup(name: String): String {
// return UUID.randomUUID().toString().also {
// groupDao.insert(
// Group(
// id = it,
// name = name,
// accountId = context.currentAccountId
// )
// )
// }
// }
//
// override suspend fun sync() {
// applicationScope.launch(dispatcherDefault) {
// mutex.withLock {
// val accountId = context.currentAccountId
//
// updateSyncState {
// it.copy(
// feedCount = 1,
// syncedCount = 1,
// currentFeedName = "Fever"
// )
// }
//
// if (feedDao.queryAll(accountId).isNullOrEmpty()) {
// // Temporary add feeds
// val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
// val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
// Log.i("RLog", "Fever groups: $feverGroupsBody")
// feverGroupsBody.groups.forEach {
// groupDao.insert(
// Group(
// id = accountId.spacerDollar(it.id),
// name = it.title,
// accountId = accountId,
// )
// )
// }
// val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
// feverGroupsBody.feeds_groups.forEach { item ->
// item.feed_ids
// .split(",")
// .map { it.toInt() }
// .forEach { id ->
// feverFeedsGroupsMap[id] = item.group_id
// }
// }
// val feeds = feverFeeds.map {
// Feed(
// id = accountId.spacerDollar(it.id),
// name = it.title,
// url = it.url,
// groupId = feverFeedsGroupsMap[it.id].toString(),
// accountId = accountId
// )
// }
// feedDao.insertList(feeds)
// }
//
// // Add articles
// val articles = mutableListOf<Article>()
// feverApiDataSource.itemsBySince(since = 1647444325925621L)
// .execute().body()!!.items
// .forEach {
// articles.add(
// Article(
// id = accountId.spacerDollar(it.id),
// date = Date(it.created_on_time * 1000),
// title = it.title,
// author = it.author,
// rawDescription = it.html,
// shortDescription = (
// Readability4JExtended("", it.html)
// .parse().textContent ?: ""
// ).take(100).trim(),
// link = it.url,
// accountId = accountId,
// feedId = it.feed_id.toString(),
// isUnread = it.is_read == 0,
// isStarred = it.is_saved == 1,
// )
// )
// }
// articleDao.insertList(articles)
//
// // Complete sync
// accountDao.update(accountDao.queryById(accountId)!!.apply {
// updateAt = Date()
// })
// updateSyncState {
// it.copy(
// feedCount = 0,
// syncedCount = 0,
// currentFeedName = ""
// )
// }
// }
// }
// }
//}

View File

@ -9,9 +9,14 @@ import android.content.Intent
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import me.ash.reader.MainActivity
import me.ash.reader.R
import me.ash.reader.data.dao.AccountDao
@ -21,9 +26,9 @@ import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Article
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Group
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherDefault
import me.ash.reader.data.module.DispatcherIO
import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.page.common.ExtraName
@ -40,8 +45,6 @@ class LocalRssRepository @Inject constructor(
private val rssNetworkDataSource: RssNetworkDataSource,
private val accountDao: AccountDao,
private val groupDao: GroupDao,
@ApplicationScope
private val applicationScope: CoroutineScope,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@DispatcherIO
@ -89,13 +92,13 @@ class LocalRssRepository @Inject constructor(
}
}
override suspend fun sync() {
applicationScope.launch(dispatcherDefault) {
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
return withContext(dispatcherDefault) {
val preTime = System.currentTimeMillis()
val accountId = context.currentAccountId
val articles = mutableListOf<Article>()
feedDao.queryAll(accountId)
.also { feed -> updateSyncState { it.copy(feedCount = feed.size) } }
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
.map { feed -> async { syncFeed(feed) } }
.awaitAll()
.forEach {
@ -114,14 +117,9 @@ class LocalRssRepository @Inject constructor(
}
)
}
updateSyncState {
it.copy(
feedCount = 0,
syncedCount = 0,
currentFeedName = ""
)
}
}.join()
coroutineWorker.setProgress(setIsSyncing(false))
ListenableWorker.Result.success()
}
}
data class ArticleNotify(
@ -146,12 +144,6 @@ class LocalRssRepository @Inject constructor(
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
return ArticleNotify(listOf(), false)
}
updateSyncState {
it.copy(
syncedCount = it.syncedCount + 1,
currentFeedName = feed.name
)
}
return ArticleNotify(
articles = articles,
isNotify = articles.isNotEmpty() && feed.isNotification

View File

@ -10,13 +10,13 @@ class RssRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val localRssRepository: LocalRssRepository,
private val feverRssRepository: FeverRssRepository,
// private val feverRssRepository: FeverRssRepository,
// private val googleReaderRssRepository: GoogleReaderRssRepository,
) {
fun get() = when (context.currentAccountType) {
Account.Type.LOCAL -> localRssRepository
// Account.Type.LOCAL -> feverRssRepository
Account.Type.FEVER -> feverRssRepository
// Account.Type.FEVER -> feverRssRepository
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
else -> throw IllegalStateException("Unknown account type: ${context.currentAccountType}")
}

View File

@ -36,7 +36,6 @@ fun HomePage(
val scope = rememberCoroutineScope()
val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
val syncState = homeViewModel.syncState.collectAsStateValue()
var openArticleId by rememberSaveable {
mutableStateOf(intent?.extras?.get(ExtraName.ARTICLE_ID)?.toString() ?: "")
@ -95,8 +94,8 @@ fun HomePage(
{
FeedsPage(
navController = navController,
syncWorkLiveData = homeViewModel.syncWorkLiveData,
filterState = filterState,
syncState = syncState,
onSyncClick = {
homeViewModel.dispatch(HomeViewAction.Sync)
},
@ -116,6 +115,7 @@ fun HomePage(
{
FlowPage(
navController = navController,
syncWorkLiveData = homeViewModel.syncWorkLiveData,
filterState = filterState,
onScrollToPage = {
homeViewModel.dispatch(

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.page.home
import androidx.lifecycle.ViewModel
import androidx.work.WorkManager
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import dagger.hilt.android.lifecycle.HiltViewModel
@ -12,8 +13,8 @@ import kotlinx.coroutines.flow.update
import me.ash.reader.data.entity.Feed
import me.ash.reader.data.entity.Filter
import me.ash.reader.data.entity.Group
import me.ash.reader.data.repository.AbstractRssRepository
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.SyncWorker
import me.ash.reader.ui.ext.animateScrollToPage
import javax.inject.Inject
@ -21,6 +22,7 @@ import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val rssRepository: RssRepository,
private val workManager: WorkManager,
) : ViewModel() {
private val _viewState = MutableStateFlow(HomeViewState())
@ -29,7 +31,7 @@ class HomeViewModel @Inject constructor(
private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow()
val syncState = AbstractRssRepository.syncState
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
fun dispatch(action: HomeViewAction) {
when (action) {

View File

@ -2,6 +2,7 @@ package me.ash.reader.ui.page.home.feeds
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@ -14,20 +15,21 @@ import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.LiveData
import androidx.navigation.NavHostController
import androidx.work.WorkInfo
import me.ash.reader.R
import me.ash.reader.data.repository.AbstractRssRepository
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.Banner
import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.ext.collectAsStateValue
@ -48,8 +50,8 @@ fun FeedsPage(
modifier: Modifier = Modifier,
navController: NavHostController,
feedsViewModel: FeedsViewModel = hiltViewModel(),
syncWorkLiveData: LiveData<WorkInfo>,
filterState: FilterState,
syncState: AbstractRssRepository.SyncState,
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
onSyncClick: () -> Unit = {},
onFilterChange: (filterState: FilterState) -> Unit = {},
@ -58,6 +60,12 @@ fun FeedsPage(
val context = LocalContext.current
val viewState = feedsViewModel.viewState.collectAsStateValue()
val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) }
syncWorkLiveData.observe(owner) {
it?.let { isSyncing = it.progress.getIsSyncing() }
}
val infiniteTransition = rememberInfiniteTransition()
val angle by infiniteTransition.animateFloat(
initialValue = 0f,
@ -108,12 +116,12 @@ fun FeedsPage(
},
actions = {
IconButton(onClick = {
if (syncState.isNotSyncing) {
if (!isSyncing) {
onSyncClick()
}
}) {
Icon(
modifier = Modifier.rotate(if (syncState.isSyncing) angle else 0f),
modifier = Modifier.rotate(if (isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh,
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
@ -142,7 +150,7 @@ fun FeedsPage(
start = 24.dp,
top = 48.dp,
end = 24.dp,
bottom = 24.dp
// bottom = 24.dp
)
.pointerInput(Unit) {
detectTapGestures(
@ -158,6 +166,26 @@ fun FeedsPage(
overflow = TextOverflow.Ellipsis,
)
}
item {
AnimatedVisibility(
visible = isSyncing,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text(
modifier = Modifier.padding(
start = 24.dp,
top = 0.dp,
end = 24.dp,
bottom = 0.dp
),
text = stringResource(R.string.syncing),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
Spacer(modifier = Modifier.height(24.dp))
}
item {
Banner(
title = filterState.filter.getName(),

View File

@ -5,6 +5,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -13,8 +16,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.formatAsString
@ -49,12 +54,29 @@ fun ArticleItem(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
Row(
modifier = Modifier.padding(start = 6.dp),
text = articleWithFeed.article.date.formatAsString(context, onlyHourMinute = true),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
verticalAlignment = Alignment.CenterVertically,
) {
if (articleWithFeed.article.isStarred) {
Icon(
modifier = Modifier
.size(14.dp)
.padding(end = 2.dp),
imageVector = Icons.Rounded.Star,
contentDescription = stringResource(R.string.starred),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
Text(
text = articleWithFeed.article.date.formatAsString(
context,
onlyHourMinute = true
),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),

View File

@ -15,16 +15,20 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.LiveData
import androidx.navigation.NavHostController
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.work.WorkInfo
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.home.FilterBar
@ -39,6 +43,7 @@ fun FlowPage(
modifier: Modifier = Modifier,
navController: NavHostController,
flowViewModel: FlowViewModel = hiltViewModel(),
syncWorkLiveData: LiveData<WorkInfo>,
filterState: FilterState,
onFilterChange: (filterState: FilterState) -> Unit = {},
onScrollToPage: (targetPage: Int) -> Unit = {},
@ -50,6 +55,12 @@ fun FlowPage(
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
var markAsRead by remember { mutableStateOf(false) }
val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) }
syncWorkLiveData.observe(owner) {
it?.let { isSyncing = it.progress.getIsSyncing() }
}
LaunchedEffect(filterState) {
flowViewModel.dispatch(
FlowViewAction.FetchData(filterState)
@ -138,7 +149,7 @@ fun FlowPage(
start = if (true) 54.dp else 24.dp,
top = 48.dp,
end = 24.dp,
bottom = 24.dp
// bottom = 24.dp
),
text = when {
filterState.group != null -> filterState.group.name
@ -151,6 +162,26 @@ fun FlowPage(
overflow = TextOverflow.Ellipsis,
)
}
item {
AnimatedVisibility(
visible = isSyncing,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text(
modifier = Modifier.padding(
start = if (true) 54.dp else 24.dp,
top = 0.dp,
end = 24.dp,
bottom = 0.dp
),
text = stringResource(R.string.syncing),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
Spacer(modifier = Modifier.height(24.dp))
}
item {
AnimatedVisibility(
visible = markAsRead,

View File

@ -7,6 +7,7 @@
<string name="starred">已加星标</string>
<string name="starred_desc">%1$d 项已加星标</string>
<string name="feeds">分组</string>
<string name="syncing">正在同步…</string>
<string name="expand_less">收缩</string>
<string name="expand_more">展开</string>
<string name="confirm">确认</string>

View File

@ -7,6 +7,7 @@
<string name="starred">Starred</string>
<string name="starred_desc">%1$d Starred Items</string>
<string name="feeds">Feeds</string>
<string name="syncing">Syncing…</string>
<string name="expand_less">Expand Less</string>
<string name="expand_more">Expand More</string>
<string name="confirm">Confirm</string>