Improve SyncState
This commit is contained in:
parent
54506e5019
commit
5f11616c6a
|
@ -48,8 +48,8 @@ class App : Application(), Configuration.Provider {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var localRssRepository: LocalRssRepository
|
lateinit var localRssRepository: LocalRssRepository
|
||||||
|
|
||||||
@Inject
|
// @Inject
|
||||||
lateinit var feverRssRepository: FeverRssRepository
|
// lateinit var feverRssRepository: FeverRssRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlRepository: OpmlRepository
|
lateinit var opmlRepository: OpmlRepository
|
||||||
|
|
|
@ -8,8 +8,8 @@ import androidx.work.*
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import me.ash.reader.data.dao.AccountDao
|
import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
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.entity.*
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
abstract class AbstractRssRepository constructor(
|
abstract class AbstractRssRepository constructor(
|
||||||
|
@ -29,22 +30,13 @@ abstract class AbstractRssRepository constructor(
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
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 updateArticleInfo(article: Article)
|
||||||
|
|
||||||
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
|
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
|
||||||
|
|
||||||
abstract suspend fun addGroup(name: String): String
|
abstract suspend fun addGroup(name: String): String
|
||||||
|
|
||||||
abstract suspend fun sync()
|
abstract suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result
|
||||||
|
|
||||||
fun doSync() {
|
fun doSync() {
|
||||||
workManager.enqueueUniquePeriodicWork(
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
@ -148,17 +140,6 @@ abstract class AbstractRssRepository constructor(
|
||||||
articleDao.deleteByFeedId(context.currentAccountId, feed.id)
|
articleDao.deleteByFeedId(context.currentAccountId, feed.id)
|
||||||
feedDao.delete(feed)
|
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
|
@HiltWorker
|
||||||
|
@ -170,18 +151,24 @@ class SyncWorker @AssistedInject constructor(
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Log.i("RLog", "doWork: ")
|
Log.i("RLog", "doWork: ")
|
||||||
rssRepository.get().sync()
|
return rssRepository.get().sync(this)
|
||||||
return Result.success()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val WORK_NAME = "article.sync"
|
const val WORK_NAME = "article.sync"
|
||||||
|
|
||||||
|
val UUID: UUID
|
||||||
|
|
||||||
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||||
15, TimeUnit.MINUTES
|
15, TimeUnit.MINUTES
|
||||||
).setConstraints(
|
).setConstraints(
|
||||||
Constraints.Builder()
|
Constraints.Builder()
|
||||||
.build()
|
.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
|
//package me.ash.reader.data.repository
|
||||||
|
//
|
||||||
import android.content.Context
|
//import android.content.Context
|
||||||
import android.util.Log
|
//import android.util.Log
|
||||||
import androidx.work.WorkManager
|
//import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
//import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
//import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
//import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
//import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.withLock
|
//import kotlinx.coroutines.sync.withLock
|
||||||
import me.ash.reader.data.dao.AccountDao
|
//import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
//import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
//import me.ash.reader.data.dao.FeedDao
|
||||||
import me.ash.reader.data.dao.GroupDao
|
//import me.ash.reader.data.dao.GroupDao
|
||||||
import me.ash.reader.data.entity.Article
|
//import me.ash.reader.data.entity.Article
|
||||||
import me.ash.reader.data.entity.Feed
|
//import me.ash.reader.data.entity.Feed
|
||||||
import me.ash.reader.data.entity.Group
|
//import me.ash.reader.data.entity.Group
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
//import me.ash.reader.data.module.ApplicationScope
|
||||||
import me.ash.reader.data.module.DispatcherDefault
|
//import me.ash.reader.data.module.DispatcherDefault
|
||||||
import me.ash.reader.data.module.DispatcherIO
|
//import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
//import me.ash.reader.data.source.FeverApiDataSource
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
//import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
//import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.ext.spacerDollar
|
//import me.ash.reader.ui.ext.spacerDollar
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
//import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import java.util.*
|
//import java.util.*
|
||||||
import javax.inject.Inject
|
//import javax.inject.Inject
|
||||||
import kotlin.collections.set
|
//import kotlin.collections.set
|
||||||
|
//
|
||||||
class FeverRssRepository @Inject constructor(
|
//class FeverRssRepository @Inject constructor(
|
||||||
@ApplicationContext
|
// @ApplicationContext
|
||||||
private val context: Context,
|
// private val context: Context,
|
||||||
private val articleDao: ArticleDao,
|
// private val articleDao: ArticleDao,
|
||||||
private val feedDao: FeedDao,
|
// private val feedDao: FeedDao,
|
||||||
private val groupDao: GroupDao,
|
// private val groupDao: GroupDao,
|
||||||
private val rssHelper: RssHelper,
|
// private val rssHelper: RssHelper,
|
||||||
private val feverApiDataSource: FeverApiDataSource,
|
// private val feverApiDataSource: FeverApiDataSource,
|
||||||
private val accountDao: AccountDao,
|
// private val accountDao: AccountDao,
|
||||||
rssNetworkDataSource: RssNetworkDataSource,
|
// rssNetworkDataSource: RssNetworkDataSource,
|
||||||
@ApplicationScope
|
// @ApplicationScope
|
||||||
private val applicationScope: CoroutineScope,
|
// private val applicationScope: CoroutineScope,
|
||||||
@DispatcherDefault
|
// @DispatcherDefault
|
||||||
private val dispatcherDefault: CoroutineDispatcher,
|
// private val dispatcherDefault: CoroutineDispatcher,
|
||||||
@DispatcherIO
|
// @DispatcherIO
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
// private val dispatcherIO: CoroutineDispatcher,
|
||||||
workManager: WorkManager,
|
// workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
//) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
// context, accountDao, articleDao, groupDao,
|
||||||
feedDao, rssNetworkDataSource, workManager,
|
// feedDao, rssNetworkDataSource, workManager,
|
||||||
dispatcherIO
|
// dispatcherIO
|
||||||
) {
|
//) {
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
// override suspend fun updateArticleInfo(article: Article) {
|
||||||
articleDao.update(article)
|
// articleDao.update(article)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
// override suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
||||||
feedDao.insert(feed)
|
// feedDao.insert(feed)
|
||||||
articleDao.insertList(articles.map {
|
// articleDao.insertList(articles.map {
|
||||||
it.copy(feedId = feed.id)
|
// it.copy(feedId = feed.id)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override suspend fun addGroup(name: String): String {
|
// override suspend fun addGroup(name: String): String {
|
||||||
return UUID.randomUUID().toString().also {
|
// return UUID.randomUUID().toString().also {
|
||||||
groupDao.insert(
|
// groupDao.insert(
|
||||||
Group(
|
// Group(
|
||||||
id = it,
|
// id = it,
|
||||||
name = name,
|
// name = name,
|
||||||
accountId = context.currentAccountId
|
// accountId = context.currentAccountId
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override suspend fun sync() {
|
// override suspend fun sync() {
|
||||||
applicationScope.launch(dispatcherDefault) {
|
// applicationScope.launch(dispatcherDefault) {
|
||||||
mutex.withLock {
|
// mutex.withLock {
|
||||||
val accountId = context.currentAccountId
|
// val accountId = context.currentAccountId
|
||||||
|
//
|
||||||
updateSyncState {
|
// updateSyncState {
|
||||||
it.copy(
|
// it.copy(
|
||||||
feedCount = 1,
|
// feedCount = 1,
|
||||||
syncedCount = 1,
|
// syncedCount = 1,
|
||||||
currentFeedName = "Fever"
|
// currentFeedName = "Fever"
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
|
// if (feedDao.queryAll(accountId).isNullOrEmpty()) {
|
||||||
// Temporary add feeds
|
// // Temporary add feeds
|
||||||
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
|
// val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
|
||||||
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
|
// val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
|
||||||
Log.i("RLog", "Fever groups: $feverGroupsBody")
|
// Log.i("RLog", "Fever groups: $feverGroupsBody")
|
||||||
feverGroupsBody.groups.forEach {
|
// feverGroupsBody.groups.forEach {
|
||||||
groupDao.insert(
|
// groupDao.insert(
|
||||||
Group(
|
// Group(
|
||||||
id = accountId.spacerDollar(it.id),
|
// id = accountId.spacerDollar(it.id),
|
||||||
name = it.title,
|
// name = it.title,
|
||||||
accountId = accountId,
|
// accountId = accountId,
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
|
// val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
|
||||||
feverGroupsBody.feeds_groups.forEach { item ->
|
// feverGroupsBody.feeds_groups.forEach { item ->
|
||||||
item.feed_ids
|
// item.feed_ids
|
||||||
.split(",")
|
// .split(",")
|
||||||
.map { it.toInt() }
|
// .map { it.toInt() }
|
||||||
.forEach { id ->
|
// .forEach { id ->
|
||||||
feverFeedsGroupsMap[id] = item.group_id
|
// feverFeedsGroupsMap[id] = item.group_id
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
val feeds = feverFeeds.map {
|
// val feeds = feverFeeds.map {
|
||||||
Feed(
|
// Feed(
|
||||||
id = accountId.spacerDollar(it.id),
|
// id = accountId.spacerDollar(it.id),
|
||||||
name = it.title,
|
// name = it.title,
|
||||||
url = it.url,
|
// url = it.url,
|
||||||
groupId = feverFeedsGroupsMap[it.id].toString(),
|
// groupId = feverFeedsGroupsMap[it.id].toString(),
|
||||||
accountId = accountId
|
// accountId = accountId
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
feedDao.insertList(feeds)
|
// feedDao.insertList(feeds)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Add articles
|
// // Add articles
|
||||||
val articles = mutableListOf<Article>()
|
// val articles = mutableListOf<Article>()
|
||||||
feverApiDataSource.itemsBySince(since = 1647444325925621L)
|
// feverApiDataSource.itemsBySince(since = 1647444325925621L)
|
||||||
.execute().body()!!.items
|
// .execute().body()!!.items
|
||||||
.forEach {
|
// .forEach {
|
||||||
articles.add(
|
// articles.add(
|
||||||
Article(
|
// Article(
|
||||||
id = accountId.spacerDollar(it.id),
|
// id = accountId.spacerDollar(it.id),
|
||||||
date = Date(it.created_on_time * 1000),
|
// date = Date(it.created_on_time * 1000),
|
||||||
title = it.title,
|
// title = it.title,
|
||||||
author = it.author,
|
// author = it.author,
|
||||||
rawDescription = it.html,
|
// rawDescription = it.html,
|
||||||
shortDescription = (
|
// shortDescription = (
|
||||||
Readability4JExtended("", it.html)
|
// Readability4JExtended("", it.html)
|
||||||
.parse().textContent ?: ""
|
// .parse().textContent ?: ""
|
||||||
).take(100).trim(),
|
// ).take(100).trim(),
|
||||||
link = it.url,
|
// link = it.url,
|
||||||
accountId = accountId,
|
// accountId = accountId,
|
||||||
feedId = it.feed_id.toString(),
|
// feedId = it.feed_id.toString(),
|
||||||
isUnread = it.is_read == 0,
|
// isUnread = it.is_read == 0,
|
||||||
isStarred = it.is_saved == 1,
|
// isStarred = it.is_saved == 1,
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
articleDao.insertList(articles)
|
// articleDao.insertList(articles)
|
||||||
|
//
|
||||||
// Complete sync
|
// // Complete sync
|
||||||
accountDao.update(accountDao.queryById(accountId)!!.apply {
|
// accountDao.update(accountDao.queryById(accountId)!!.apply {
|
||||||
updateAt = Date()
|
// updateAt = Date()
|
||||||
})
|
// })
|
||||||
updateSyncState {
|
// updateSyncState {
|
||||||
it.copy(
|
// it.copy(
|
||||||
feedCount = 0,
|
// feedCount = 0,
|
||||||
syncedCount = 0,
|
// syncedCount = 0,
|
||||||
currentFeedName = ""
|
// currentFeedName = ""
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
|
@ -9,9 +9,14 @@ import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
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.MainActivity
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.dao.AccountDao
|
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.Article
|
||||||
import me.ash.reader.data.entity.Feed
|
import me.ash.reader.data.entity.Feed
|
||||||
import me.ash.reader.data.entity.Group
|
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.DispatcherDefault
|
||||||
import me.ash.reader.data.module.DispatcherIO
|
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.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.ui.ext.currentAccountId
|
import me.ash.reader.ui.ext.currentAccountId
|
||||||
import me.ash.reader.ui.page.common.ExtraName
|
import me.ash.reader.ui.page.common.ExtraName
|
||||||
|
@ -40,8 +45,6 @@ class LocalRssRepository @Inject constructor(
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
@ApplicationScope
|
|
||||||
private val applicationScope: CoroutineScope,
|
|
||||||
@DispatcherDefault
|
@DispatcherDefault
|
||||||
private val dispatcherDefault: CoroutineDispatcher,
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
@DispatcherIO
|
@DispatcherIO
|
||||||
|
@ -89,13 +92,13 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sync() {
|
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
|
||||||
applicationScope.launch(dispatcherDefault) {
|
return withContext(dispatcherDefault) {
|
||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val articles = mutableListOf<Article>()
|
val articles = mutableListOf<Article>()
|
||||||
feedDao.queryAll(accountId)
|
feedDao.queryAll(accountId)
|
||||||
.also { feed -> updateSyncState { it.copy(feedCount = feed.size) } }
|
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
|
||||||
.map { feed -> async { syncFeed(feed) } }
|
.map { feed -> async { syncFeed(feed) } }
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
.forEach {
|
.forEach {
|
||||||
|
@ -114,14 +117,9 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateSyncState {
|
coroutineWorker.setProgress(setIsSyncing(false))
|
||||||
it.copy(
|
ListenableWorker.Result.success()
|
||||||
feedCount = 0,
|
}
|
||||||
syncedCount = 0,
|
|
||||||
currentFeedName = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.join()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ArticleNotify(
|
data class ArticleNotify(
|
||||||
|
@ -146,12 +144,6 @@ class LocalRssRepository @Inject constructor(
|
||||||
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
||||||
return ArticleNotify(listOf(), false)
|
return ArticleNotify(listOf(), false)
|
||||||
}
|
}
|
||||||
updateSyncState {
|
|
||||||
it.copy(
|
|
||||||
syncedCount = it.syncedCount + 1,
|
|
||||||
currentFeedName = feed.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return ArticleNotify(
|
return ArticleNotify(
|
||||||
articles = articles,
|
articles = articles,
|
||||||
isNotify = articles.isNotEmpty() && feed.isNotification
|
isNotify = articles.isNotEmpty() && feed.isNotification
|
||||||
|
|
|
@ -10,13 +10,13 @@ class RssRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val localRssRepository: LocalRssRepository,
|
private val localRssRepository: LocalRssRepository,
|
||||||
private val feverRssRepository: FeverRssRepository,
|
// private val feverRssRepository: FeverRssRepository,
|
||||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
) {
|
) {
|
||||||
fun get() = when (context.currentAccountType) {
|
fun get() = when (context.currentAccountType) {
|
||||||
Account.Type.LOCAL -> localRssRepository
|
Account.Type.LOCAL -> localRssRepository
|
||||||
// Account.Type.LOCAL -> feverRssRepository
|
// Account.Type.LOCAL -> feverRssRepository
|
||||||
Account.Type.FEVER -> feverRssRepository
|
// Account.Type.FEVER -> feverRssRepository
|
||||||
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
||||||
else -> throw IllegalStateException("Unknown account type: ${context.currentAccountType}")
|
else -> throw IllegalStateException("Unknown account type: ${context.currentAccountType}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@ fun HomePage(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val viewState = homeViewModel.viewState.collectAsStateValue()
|
val viewState = homeViewModel.viewState.collectAsStateValue()
|
||||||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
val filterState = homeViewModel.filterState.collectAsStateValue()
|
||||||
val syncState = homeViewModel.syncState.collectAsStateValue()
|
|
||||||
|
|
||||||
var openArticleId by rememberSaveable {
|
var openArticleId by rememberSaveable {
|
||||||
mutableStateOf(intent?.extras?.get(ExtraName.ARTICLE_ID)?.toString() ?: "")
|
mutableStateOf(intent?.extras?.get(ExtraName.ARTICLE_ID)?.toString() ?: "")
|
||||||
|
@ -95,8 +94,8 @@ fun HomePage(
|
||||||
{
|
{
|
||||||
FeedsPage(
|
FeedsPage(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
syncWorkLiveData = homeViewModel.syncWorkLiveData,
|
||||||
filterState = filterState,
|
filterState = filterState,
|
||||||
syncState = syncState,
|
|
||||||
onSyncClick = {
|
onSyncClick = {
|
||||||
homeViewModel.dispatch(HomeViewAction.Sync)
|
homeViewModel.dispatch(HomeViewAction.Sync)
|
||||||
},
|
},
|
||||||
|
@ -116,6 +115,7 @@ fun HomePage(
|
||||||
{
|
{
|
||||||
FlowPage(
|
FlowPage(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
syncWorkLiveData = homeViewModel.syncWorkLiveData,
|
||||||
filterState = filterState,
|
filterState = filterState,
|
||||||
onScrollToPage = {
|
onScrollToPage = {
|
||||||
homeViewModel.dispatch(
|
homeViewModel.dispatch(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package me.ash.reader.ui.page.home
|
package me.ash.reader.ui.page.home
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.work.WorkManager
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import com.google.accompanist.pager.PagerState
|
import com.google.accompanist.pager.PagerState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.Feed
|
||||||
import me.ash.reader.data.entity.Filter
|
import me.ash.reader.data.entity.Filter
|
||||||
import me.ash.reader.data.entity.Group
|
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.RssRepository
|
||||||
|
import me.ash.reader.data.repository.SyncWorker
|
||||||
import me.ash.reader.ui.ext.animateScrollToPage
|
import me.ash.reader.ui.ext.animateScrollToPage
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
|
private val workManager: WorkManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _viewState = MutableStateFlow(HomeViewState())
|
private val _viewState = MutableStateFlow(HomeViewState())
|
||||||
|
@ -29,7 +31,7 @@ class HomeViewModel @Inject constructor(
|
||||||
private val _filterState = MutableStateFlow(FilterState())
|
private val _filterState = MutableStateFlow(FilterState())
|
||||||
val filterState = _filterState.asStateFlow()
|
val filterState = _filterState.asStateFlow()
|
||||||
|
|
||||||
val syncState = AbstractRssRepository.syncState
|
val syncWorkLiveData = workManager.getWorkInfoByIdLiveData(SyncWorker.UUID)
|
||||||
|
|
||||||
fun dispatch(action: HomeViewAction) {
|
fun dispatch(action: HomeViewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
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.ArrowBack
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import me.ash.reader.R
|
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.Banner
|
||||||
import me.ash.reader.ui.component.Subtitle
|
import me.ash.reader.ui.component.Subtitle
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
|
@ -48,8 +50,8 @@ fun FeedsPage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
feedsViewModel: FeedsViewModel = hiltViewModel(),
|
feedsViewModel: FeedsViewModel = hiltViewModel(),
|
||||||
|
syncWorkLiveData: LiveData<WorkInfo>,
|
||||||
filterState: FilterState,
|
filterState: FilterState,
|
||||||
syncState: AbstractRssRepository.SyncState,
|
|
||||||
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
|
subscribeViewModel: SubscribeViewModel = hiltViewModel(),
|
||||||
onSyncClick: () -> Unit = {},
|
onSyncClick: () -> Unit = {},
|
||||||
onFilterChange: (filterState: FilterState) -> Unit = {},
|
onFilterChange: (filterState: FilterState) -> Unit = {},
|
||||||
|
@ -58,6 +60,12 @@ fun FeedsPage(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val viewState = feedsViewModel.viewState.collectAsStateValue()
|
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 infiniteTransition = rememberInfiniteTransition()
|
||||||
val angle by infiniteTransition.animateFloat(
|
val angle by infiniteTransition.animateFloat(
|
||||||
initialValue = 0f,
|
initialValue = 0f,
|
||||||
|
@ -108,12 +116,12 @@ fun FeedsPage(
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (syncState.isNotSyncing) {
|
if (!isSyncing) {
|
||||||
onSyncClick()
|
onSyncClick()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.rotate(if (syncState.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),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
@ -142,7 +150,7 @@ fun FeedsPage(
|
||||||
start = 24.dp,
|
start = 24.dp,
|
||||||
top = 48.dp,
|
top = 48.dp,
|
||||||
end = 24.dp,
|
end = 24.dp,
|
||||||
bottom = 24.dp
|
// bottom = 24.dp
|
||||||
)
|
)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
|
@ -158,6 +166,26 @@ fun FeedsPage(
|
||||||
overflow = TextOverflow.Ellipsis,
|
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 {
|
item {
|
||||||
Banner(
|
Banner(
|
||||||
title = filterState.filter.getName(),
|
title = filterState.filter.getName(),
|
||||||
|
|
|
@ -5,6 +5,9 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
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
|
||||||
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.ArticleWithFeed
|
import me.ash.reader.data.entity.ArticleWithFeed
|
||||||
import me.ash.reader.ui.ext.formatAsString
|
import me.ash.reader.ui.ext.formatAsString
|
||||||
|
|
||||||
|
@ -49,12 +54,29 @@ fun ArticleItem(
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Text(
|
Row(
|
||||||
modifier = Modifier.padding(start = 6.dp),
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
text = articleWithFeed.article.date.formatAsString(context, onlyHourMinute = true),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
) {
|
||||||
style = MaterialTheme.typography.labelMedium,
|
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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
|
@ -15,16 +15,20 @@ 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.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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.entity.ArticleWithFeed
|
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.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.getName
|
import me.ash.reader.ui.ext.getName
|
||||||
import me.ash.reader.ui.page.home.FilterBar
|
import me.ash.reader.ui.page.home.FilterBar
|
||||||
|
@ -39,6 +43,7 @@ fun FlowPage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
flowViewModel: FlowViewModel = hiltViewModel(),
|
flowViewModel: FlowViewModel = hiltViewModel(),
|
||||||
|
syncWorkLiveData: LiveData<WorkInfo>,
|
||||||
filterState: FilterState,
|
filterState: FilterState,
|
||||||
onFilterChange: (filterState: FilterState) -> Unit = {},
|
onFilterChange: (filterState: FilterState) -> Unit = {},
|
||||||
onScrollToPage: (targetPage: Int) -> Unit = {},
|
onScrollToPage: (targetPage: Int) -> Unit = {},
|
||||||
|
@ -50,6 +55,12 @@ fun FlowPage(
|
||||||
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
|
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
|
||||||
var markAsRead by remember { mutableStateOf(false) }
|
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) {
|
LaunchedEffect(filterState) {
|
||||||
flowViewModel.dispatch(
|
flowViewModel.dispatch(
|
||||||
FlowViewAction.FetchData(filterState)
|
FlowViewAction.FetchData(filterState)
|
||||||
|
@ -138,7 +149,7 @@ fun FlowPage(
|
||||||
start = if (true) 54.dp else 24.dp,
|
start = if (true) 54.dp else 24.dp,
|
||||||
top = 48.dp,
|
top = 48.dp,
|
||||||
end = 24.dp,
|
end = 24.dp,
|
||||||
bottom = 24.dp
|
// bottom = 24.dp
|
||||||
),
|
),
|
||||||
text = when {
|
text = when {
|
||||||
filterState.group != null -> filterState.group.name
|
filterState.group != null -> filterState.group.name
|
||||||
|
@ -151,6 +162,26 @@ fun FlowPage(
|
||||||
overflow = TextOverflow.Ellipsis,
|
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 {
|
item {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = markAsRead,
|
visible = markAsRead,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<string name="starred">已加星标</string>
|
<string name="starred">已加星标</string>
|
||||||
<string name="starred_desc">%1$d 项已加星标</string>
|
<string name="starred_desc">%1$d 项已加星标</string>
|
||||||
<string name="feeds">分组</string>
|
<string name="feeds">分组</string>
|
||||||
|
<string name="syncing">正在同步…</string>
|
||||||
<string name="expand_less">收缩</string>
|
<string name="expand_less">收缩</string>
|
||||||
<string name="expand_more">展开</string>
|
<string name="expand_more">展开</string>
|
||||||
<string name="confirm">确认</string>
|
<string name="confirm">确认</string>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<string name="starred">Starred</string>
|
<string name="starred">Starred</string>
|
||||||
<string name="starred_desc">%1$d Starred Items</string>
|
<string name="starred_desc">%1$d Starred Items</string>
|
||||||
<string name="feeds">Feeds</string>
|
<string name="feeds">Feeds</string>
|
||||||
|
<string name="syncing">Syncing…</string>
|
||||||
<string name="expand_less">Expand Less</string>
|
<string name="expand_less">Expand Less</string>
|
||||||
<string name="expand_more">Expand More</string>
|
<string name="expand_more">Expand More</string>
|
||||||
<string name="confirm">Confirm</string>
|
<string name="confirm">Confirm</string>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user