From 5f11616c6aa4bff11ad7212de7d0bce1e4aa6a8e Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 3 Apr 2022 18:15:28 +0800 Subject: [PATCH] Improve SyncState --- app/src/main/java/me/ash/reader/App.kt | 4 +- .../data/repository/AbstractRssRepository.kt | 39 +-- .../data/repository/FeverRssRepository.kt | 326 +++++++++--------- .../data/repository/LocalRssRepository.kt | 34 +- .../reader/data/repository/RssRepository.kt | 4 +- .../me/ash/reader/ui/page/home/HomePage.kt | 4 +- .../ash/reader/ui/page/home/HomeViewModel.kt | 6 +- .../reader/ui/page/home/feeds/FeedsPage.kt | 44 ++- .../reader/ui/page/home/flow/ArticleItem.kt | 32 +- .../ash/reader/ui/page/home/flow/FlowPage.kt | 33 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 12 files changed, 296 insertions(+), 232 deletions(-) diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt index 6790bad..ff34591 100644 --- a/app/src/main/java/me/ash/reader/App.kt +++ b/app/src/main/java/me/ash/reader/App.kt @@ -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 diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index ba57a8d..589d434 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -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
) 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( 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) } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt index 91ea2d3..a7b8600 100644 --- a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt @@ -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
) { - 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() - 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
() - 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 = "" - ) - } - } - } - } -} \ No newline at end of file +//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
) { +// 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() +// 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
() +// 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 = "" +// ) +// } +// } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt index 6bb3b7e..de8967e 100644 --- a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt @@ -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
() 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 diff --git a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt index 0e247c1..ab0dd35 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssRepository.kt @@ -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}") } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt index d0c75c8..aa460e5 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt @@ -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( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 9af62bc..06a1a06 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -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) { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 1ab0017..bcb236a 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -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, 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(), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt index a5ef4b7..10e569e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt @@ -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(), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index 317b077..2030d63 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -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, 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, diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 541bdfc..5c3b961 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -7,6 +7,7 @@ 已加星标 %1$d 项已加星标 分组 + 正在同步… 收缩 展开 确认 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 142939f..66e0a7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Starred %1$d Starred Items Feeds + Syncing… Expand Less Expand More Confirm