Improve SyncState
This commit is contained in:
parent
54506e5019
commit
5f11616c6a
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 = ""
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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 = ""
|
||||
)
|
||||
coroutineWorker.setProgress(setIsSyncing(false))
|
||||
ListenableWorker.Result.success()
|
||||
}
|
||||
}.join()
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,13 +54,30 @@ fun ArticleItem(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
text = articleWithFeed.article.date.formatAsString(context, onlyHourMinute = true),
|
||||
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(),
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user