Refactor local RSS repository to support other RSS API data sources
This commit is contained in:
parent
a5b81f7f23
commit
d105735453
|
@ -5,10 +5,7 @@ import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.repository.AccountRepository
|
import me.ash.reader.data.repository.*
|
||||||
import me.ash.reader.data.repository.ArticleRepository
|
|
||||||
import me.ash.reader.data.repository.OpmlRepository
|
|
||||||
import me.ash.reader.data.repository.RssRepository
|
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||||
import me.ash.reader.data.source.ReaderDatabase
|
import me.ash.reader.data.source.ReaderDatabase
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
@ -26,11 +23,14 @@ class App : Application() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var rssNetworkDataSource: RssNetworkDataSource
|
lateinit var rssNetworkDataSource: RssNetworkDataSource
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var rssHelper: RssHelper
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var accountRepository: AccountRepository
|
lateinit var accountRepository: AccountRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var articleRepository: ArticleRepository
|
lateinit var localRssRepository: LocalRssRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlRepository: OpmlRepository
|
lateinit var opmlRepository: OpmlRepository
|
||||||
|
@ -42,10 +42,11 @@ class App : Application() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
if (accountRepository.isNoAccount()) {
|
if (accountRepository.isNoAccount()) {
|
||||||
val accountId = accountRepository.addDefaultAccount()
|
val account = accountRepository.addDefaultAccount()
|
||||||
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, accountId)
|
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, account.id!!)
|
||||||
|
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountType, account.type)
|
||||||
}
|
}
|
||||||
rssRepository.sync(true)
|
rssRepository.get().doSync(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -46,4 +46,9 @@ sealed class DataStoreKeys<T> {
|
||||||
override val key: Preferences.Key<Int>
|
override val key: Preferences.Key<Int>
|
||||||
get() = intPreferencesKey("currentAccountId")
|
get() = intPreferencesKey("currentAccountId")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object CurrentAccountType : DataStoreKeys<Int>() {
|
||||||
|
override val key: Preferences.Key<Int>
|
||||||
|
get() = intPreferencesKey("currentAccountType")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@ import java.util.*
|
||||||
@Entity(tableName = "account")
|
@Entity(tableName = "account")
|
||||||
data class Account(
|
data class Account(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
val id: Int? = null,
|
var id: Int? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var name: String,
|
var name: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
|
@ -18,6 +18,7 @@ data class Account(
|
||||||
) {
|
) {
|
||||||
object Type {
|
object Type {
|
||||||
const val LOCAL = 1
|
const val LOCAL = 1
|
||||||
const val FRESH_RSS = 2
|
const val FEVER = 2
|
||||||
|
const val GOOGLE_READER = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,8 +18,8 @@ import java.util.*
|
||||||
)]
|
)]
|
||||||
)
|
)
|
||||||
data class Article(
|
data class Article(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
val id: Int? = null,
|
val id: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val date: Date,
|
val date: Date,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
|
@ -35,7 +35,7 @@ data class Article(
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val link: String,
|
val link: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val feedId: Int,
|
val feedId: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val accountId: Int,
|
val accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "true")
|
@ColumnInfo(defaultValue = "true")
|
||||||
|
|
|
@ -167,7 +167,7 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
fun queryArticleWithFeedByGroupIdWhenIsAll(
|
fun queryArticleWithFeedByGroupIdWhenIsAll(
|
||||||
accountId: Int,
|
accountId: Int,
|
||||||
groupId: Int
|
groupId: String,
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
|
@ -188,7 +188,7 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
fun queryArticleWithFeedByGroupIdWhenIsStarred(
|
fun queryArticleWithFeedByGroupIdWhenIsStarred(
|
||||||
accountId: Int,
|
accountId: Int,
|
||||||
groupId: Int,
|
groupId: String,
|
||||||
isStarred: Boolean,
|
isStarred: Boolean,
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
fun queryArticleWithFeedByGroupIdWhenIsUnread(
|
fun queryArticleWithFeedByGroupIdWhenIsUnread(
|
||||||
accountId: Int,
|
accountId: Int,
|
||||||
groupId: Int,
|
groupId: String,
|
||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
fun queryArticleWithFeedByFeedIdWhenIsAll(
|
fun queryArticleWithFeedByFeedIdWhenIsAll(
|
||||||
accountId: Int,
|
accountId: Int,
|
||||||
feedId: Int
|
feedId: String
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
|
@ -238,7 +238,7 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
fun queryArticleWithFeedByFeedIdWhenIsStarred(
|
fun queryArticleWithFeedByFeedIdWhenIsStarred(
|
||||||
accountId: Int,
|
accountId: Int,
|
||||||
feedId: Int,
|
feedId: String,
|
||||||
isStarred: Boolean,
|
isStarred: Boolean,
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ interface ArticleDao {
|
||||||
)
|
)
|
||||||
fun queryArticleWithFeedByFeedIdWhenIsUnread(
|
fun queryArticleWithFeedByFeedIdWhenIsUnread(
|
||||||
accountId: Int,
|
accountId: Int,
|
||||||
feedId: Int,
|
feedId: String,
|
||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
): PagingSource<Int, ArticleWithFeed>
|
): PagingSource<Int, ArticleWithFeed>
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ interface ArticleDao {
|
||||||
ORDER BY date DESC LIMIT 1
|
ORDER BY date DESC LIMIT 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
suspend fun queryLatestByFeedId(accountId: Int, feedId: Int): Article?
|
suspend fun queryLatestByFeedId(accountId: Int, feedId: String): Article?
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
|
|
|
@ -2,6 +2,6 @@ package me.ash.reader.data.article
|
||||||
|
|
||||||
data class ImportantCount(
|
data class ImportantCount(
|
||||||
val important: Int,
|
val important: Int,
|
||||||
val feedId: Int,
|
val feedId: String,
|
||||||
val groupId: Int,
|
val groupId: String,
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,8 +14,8 @@ import me.ash.reader.data.group.Group
|
||||||
)],
|
)],
|
||||||
)
|
)
|
||||||
data class Feed(
|
data class Feed(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
val id: Int? = null,
|
val id: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val name: String,
|
val name: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
|
@ -23,7 +23,7 @@ data class Feed(
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val url: String,
|
val url: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
var groupId: Int? = null,
|
var groupId: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val accountId: Int,
|
val accountId: Int,
|
||||||
@ColumnInfo(defaultValue = "false")
|
@ColumnInfo(defaultValue = "false")
|
||||||
|
@ -33,7 +33,6 @@ data class Feed(
|
||||||
) {
|
) {
|
||||||
@Ignore
|
@Ignore
|
||||||
var important: Int? = 0
|
var important: Int? = 0
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
@ -57,11 +56,11 @@ data class Feed(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = id ?: 0
|
var result = id.hashCode()
|
||||||
result = 31 * result + name.hashCode()
|
result = 31 * result + name.hashCode()
|
||||||
result = 31 * result + (icon?.contentHashCode() ?: 0)
|
result = 31 * result + (icon?.contentHashCode() ?: 0)
|
||||||
result = 31 * result + url.hashCode()
|
result = 31 * result + url.hashCode()
|
||||||
result = 31 * result + (groupId ?: 0)
|
result = 31 * result + groupId.hashCode()
|
||||||
result = 31 * result + accountId
|
result = 31 * result + accountId
|
||||||
result = 31 * result + isNotification.hashCode()
|
result = 31 * result + isNotification.hashCode()
|
||||||
result = 31 * result + isFullContent.hashCode()
|
result = 31 * result + isFullContent.hashCode()
|
||||||
|
|
|
@ -7,8 +7,8 @@ import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@Entity(tableName = "group")
|
@Entity(tableName = "group")
|
||||||
data class Group(
|
data class Group(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
val id: Int? = null,
|
val id: String,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val name: String,
|
val name: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
|
|
|
@ -4,14 +4,26 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
|
import me.ash.reader.data.source.GoogleReaderApiDataSource
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class RssNetworkModule {
|
class RetrofitModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRssNetworkDataSource(): RssNetworkDataSource =
|
fun provideRssNetworkDataSource(): RssNetworkDataSource =
|
||||||
RssNetworkDataSource.getInstance()
|
RssNetworkDataSource.getInstance()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideFeverApiDataSource(): FeverApiDataSource =
|
||||||
|
FeverApiDataSource.getInstance()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideGoogleReaderApiDataSource(): GoogleReaderApiDataSource =
|
||||||
|
GoogleReaderApiDataSource.getInstance()
|
||||||
}
|
}
|
|
@ -3,9 +3,11 @@ package me.ash.reader.data.repository
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import androidx.work.*
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.DataStoreKeys
|
||||||
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.article.ArticleDao
|
import me.ash.reader.data.article.ArticleDao
|
||||||
import me.ash.reader.data.article.ArticleWithFeed
|
import me.ash.reader.data.article.ArticleWithFeed
|
||||||
|
@ -15,17 +17,43 @@ import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
|
import me.ash.reader.data.source.ReaderDatabase
|
||||||
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.dataStore
|
import me.ash.reader.dataStore
|
||||||
import me.ash.reader.get
|
import me.ash.reader.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ArticleRepository @Inject constructor(
|
abstract class AbstractRssRepository constructor(
|
||||||
@ApplicationContext
|
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val accountDao: AccountDao,
|
||||||
private val articleDao: ArticleDao,
|
private val articleDao: ArticleDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
private val workManager: WorkManager,
|
||||||
) {
|
) {
|
||||||
|
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 sync(
|
||||||
|
context: Context,
|
||||||
|
accountDao: AccountDao,
|
||||||
|
articleDao: ArticleDao,
|
||||||
|
feedDao: FeedDao,
|
||||||
|
rssNetworkDataSource: RssNetworkDataSource
|
||||||
|
)
|
||||||
|
|
||||||
fun pullGroups(): Flow<MutableList<Group>> {
|
fun pullGroups(): Flow<MutableList<Group>> {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
||||||
return groupDao.queryAllGroup(accountId)
|
return groupDao.queryAllGroup(accountId)
|
||||||
|
@ -38,8 +66,8 @@ class ArticleRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullArticles(
|
fun pullArticles(
|
||||||
groupId: Int? = null,
|
groupId: String? = null,
|
||||||
feedId: Int? = null,
|
feedId: String? = null,
|
||||||
isStarred: Boolean = false,
|
isStarred: Boolean = false,
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): PagingSource<Int, ArticleWithFeed> {
|
): PagingSource<Int, ArticleWithFeed> {
|
||||||
|
@ -91,18 +119,54 @@ class ArticleRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateArticleInfo(article: Article) {
|
|
||||||
articleDao.update(article)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findArticleById(id: Int): ArticleWithFeed? {
|
suspend fun findArticleById(id: Int): ArticleWithFeed? {
|
||||||
return articleDao.queryById(id)
|
return articleDao.queryById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
fun peekWork(): String {
|
||||||
val feedId = feedDao.insert(feed).toInt()
|
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
||||||
articleDao.insertList(articles.map {
|
}
|
||||||
it.copy(feedId = feedId)
|
|
||||||
})
|
suspend fun doSync(isWork: Boolean? = false) {
|
||||||
|
if (isWork == true) {
|
||||||
|
workManager.cancelAllWork()
|
||||||
|
val syncWorkerRequest: WorkRequest =
|
||||||
|
PeriodicWorkRequestBuilder<SyncWorker>(
|
||||||
|
15, TimeUnit.MINUTES
|
||||||
|
).setConstraints(
|
||||||
|
Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
).addTag("sync").build()
|
||||||
|
workManager.enqueue(syncWorkerRequest)
|
||||||
|
} else {
|
||||||
|
sync(context, accountDao, articleDao, feedDao, rssNetworkDataSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
class SyncWorker(
|
||||||
|
context: Context,
|
||||||
|
workerParams: WorkerParameters,
|
||||||
|
) : CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var rssRepository: RssRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var rssNetworkDataSource: RssNetworkDataSource
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
Log.i("RLog", "doWork: ")
|
||||||
|
val db = ReaderDatabase.getInstance(applicationContext)
|
||||||
|
rssRepository.get().sync(
|
||||||
|
applicationContext,
|
||||||
|
db.accountDao(),
|
||||||
|
db.articleDao(),
|
||||||
|
db.feedDao(),
|
||||||
|
rssNetworkDataSource
|
||||||
|
)
|
||||||
|
return Result.success()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,12 +24,12 @@ class AccountRepository @Inject constructor(
|
||||||
return accountDao.queryAll().isEmpty()
|
return accountDao.queryAll().isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addDefaultAccount(): Int {
|
suspend fun addDefaultAccount(): Account {
|
||||||
return accountDao.insert(
|
return Account(
|
||||||
Account(
|
name = "Reader You",
|
||||||
name = "Feeds",
|
|
||||||
type = Account.Type.LOCAL,
|
type = Account.Type.LOCAL,
|
||||||
)
|
).apply {
|
||||||
).toInt()
|
id = accountDao.insert(this).toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import me.ash.reader.*
|
||||||
|
import me.ash.reader.data.account.AccountDao
|
||||||
|
import me.ash.reader.data.article.Article
|
||||||
|
import me.ash.reader.data.article.ArticleDao
|
||||||
|
import me.ash.reader.data.constant.Symbol
|
||||||
|
import me.ash.reader.data.feed.Feed
|
||||||
|
import me.ash.reader.data.feed.FeedDao
|
||||||
|
import me.ash.reader.data.group.GroupDao
|
||||||
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LocalRssRepository @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val articleDao: ArticleDao,
|
||||||
|
private val feedDao: FeedDao,
|
||||||
|
private val rssHelper: RssHelper,
|
||||||
|
rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
groupDao: GroupDao,
|
||||||
|
accountDao: AccountDao,
|
||||||
|
workManager: WorkManager,
|
||||||
|
) : AbstractRssRepository(
|
||||||
|
context, accountDao, articleDao, groupDao,
|
||||||
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
|
) {
|
||||||
|
val syncState = MutableStateFlow(SyncState())
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
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 sync(
|
||||||
|
context: Context,
|
||||||
|
accountDao: AccountDao,
|
||||||
|
articleDao: ArticleDao,
|
||||||
|
feedDao: FeedDao,
|
||||||
|
rssNetworkDataSource: RssNetworkDataSource
|
||||||
|
) {
|
||||||
|
mutex.withLock {
|
||||||
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
||||||
|
?: return
|
||||||
|
val feeds = feedDao.queryAll(accountId)
|
||||||
|
val feedNotificationMap = mutableMapOf<String, Boolean>()
|
||||||
|
feeds.forEach { feed ->
|
||||||
|
feedNotificationMap[feed.id] = feed.isNotification
|
||||||
|
}
|
||||||
|
val preTime = System.currentTimeMillis()
|
||||||
|
val chunked = feeds.chunked(6)
|
||||||
|
chunked.forEachIndexed { index, item ->
|
||||||
|
item.forEach {
|
||||||
|
Log.i("RlOG", "chunked $index: ${it.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val flows = mutableListOf<Flow<List<Article>>>()
|
||||||
|
repeat(chunked.size) {
|
||||||
|
flows.add(flow {
|
||||||
|
val articles = mutableListOf<Article>()
|
||||||
|
chunked[it].forEach { feed ->
|
||||||
|
val latest = articleDao.queryLatestByFeedId(accountId, feed.id)
|
||||||
|
articles.addAll(
|
||||||
|
rssHelper.queryRssXml(
|
||||||
|
rssNetworkDataSource,
|
||||||
|
accountId,
|
||||||
|
feed,
|
||||||
|
latest?.title,
|
||||||
|
).also {
|
||||||
|
if (feed.icon == null && it.isNotEmpty()) {
|
||||||
|
rssHelper.queryRssIcon(feedDao, feed, it.first().link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
syncState.update {
|
||||||
|
it.copy(
|
||||||
|
feedCount = feeds.size,
|
||||||
|
syncedCount = syncState.value.syncedCount + 1,
|
||||||
|
currentFeedName = feed.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit(articles)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
combine(
|
||||||
|
flows
|
||||||
|
) {
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
getSystemService(
|
||||||
|
context,
|
||||||
|
NotificationManager::class.java
|
||||||
|
) as NotificationManager
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
notificationManager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE,
|
||||||
|
"文章更新",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
it.forEach { articleList ->
|
||||||
|
val ids = articleDao.insertList(articleList)
|
||||||
|
articleList.forEachIndexed { index, article ->
|
||||||
|
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
|
||||||
|
if (feedNotificationMap[article.feedId] == true) {
|
||||||
|
val builder = NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
||||||
|
).setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||||
|
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
||||||
|
.setContentTitle(article.title)
|
||||||
|
.setContentText(article.shortDescription)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentIntent(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
ids[index].toInt(),
|
||||||
|
Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
putExtra(
|
||||||
|
Symbol.EXTRA_ARTICLE_ID,
|
||||||
|
ids[index].toInt()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
notificationManager.notify(
|
||||||
|
ids[index].toInt(),
|
||||||
|
builder.build().apply {
|
||||||
|
flags = Notification.FLAG_AUTO_CANCEL
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.buffer().onCompletion {
|
||||||
|
val afterTime = System.currentTimeMillis()
|
||||||
|
Log.i("RlOG", "onCompletion: ${afterTime - preTime}")
|
||||||
|
accountDao.queryById(accountId)?.let { account ->
|
||||||
|
accountDao.update(
|
||||||
|
account.apply {
|
||||||
|
updateAt = Date()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
syncState.update {
|
||||||
|
it.copy(
|
||||||
|
feedCount = 0,
|
||||||
|
syncedCount = 0,
|
||||||
|
currentFeedName = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,8 +16,8 @@ class OpmlRepository @Inject constructor(
|
||||||
try {
|
try {
|
||||||
val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream)
|
val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream)
|
||||||
groupWithFeedList.forEach { groupWithFeed ->
|
groupWithFeedList.forEach { groupWithFeed ->
|
||||||
val id = groupDao.insert(groupWithFeed.group).toInt()
|
groupDao.insert(groupWithFeed.group)
|
||||||
groupWithFeed.feeds.forEach { it.groupId = id }
|
groupWithFeed.feeds.forEach { it.groupId = groupWithFeed.group.id }
|
||||||
feedDao.insertList(groupWithFeed.feeds)
|
feedDao.insertList(groupWithFeed.feeds)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
177
app/src/main/java/me/ash/reader/data/repository/RssHelper.kt
Normal file
177
app/src/main/java/me/ash/reader/data/repository/RssHelper.kt
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import me.ash.reader.DataStoreKeys
|
||||||
|
import me.ash.reader.data.article.Article
|
||||||
|
import me.ash.reader.data.feed.Feed
|
||||||
|
import me.ash.reader.data.feed.FeedDao
|
||||||
|
import me.ash.reader.data.feed.FeedWithArticle
|
||||||
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
import me.ash.reader.dataStore
|
||||||
|
import me.ash.reader.get
|
||||||
|
import net.dankito.readability4j.Readability4J
|
||||||
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
|
import okhttp3.*
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RssHelper @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
) {
|
||||||
|
@Throws(Exception::class)
|
||||||
|
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||||
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
||||||
|
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
||||||
|
val feed = Feed(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
name = parseRss.title!!,
|
||||||
|
url = feedLink,
|
||||||
|
groupId = UUID.randomUUID().toString(),
|
||||||
|
accountId = accountId,
|
||||||
|
)
|
||||||
|
val articles = mutableListOf<Article>()
|
||||||
|
parseRss.items.forEach {
|
||||||
|
articles.add(
|
||||||
|
Article(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
accountId = accountId,
|
||||||
|
feedId = feed.id,
|
||||||
|
date = Date(it.publishDate.toString()),
|
||||||
|
title = it.title.toString(),
|
||||||
|
author = it.author,
|
||||||
|
rawDescription = it.description.toString(),
|
||||||
|
shortDescription = (Readability4JExtended("", it.description.toString())
|
||||||
|
.parse().textContent ?: "").trim().run {
|
||||||
|
if (this.length > 100) this.substring(0, 100)
|
||||||
|
else this
|
||||||
|
},
|
||||||
|
link = it.link ?: "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return FeedWithArticle(feed, articles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseDescriptionContent(link: String, content: String): String {
|
||||||
|
val readability4J: Readability4J = Readability4JExtended(link, content)
|
||||||
|
val article = readability4J.parse()
|
||||||
|
val element = article.articleContent
|
||||||
|
return element.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseFullContent(link: String, title: String, callback: (String) -> Unit) {
|
||||||
|
OkHttpClient()
|
||||||
|
.newCall(Request.Builder().url(link).build())
|
||||||
|
.enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
callback(e.message.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
val content = response.body?.string()
|
||||||
|
val readability4J: Readability4J =
|
||||||
|
Readability4JExtended(link, content ?: "")
|
||||||
|
val articleContent = readability4J.parse().articleContent
|
||||||
|
if (articleContent == null) {
|
||||||
|
callback("")
|
||||||
|
} else {
|
||||||
|
val h1Element = articleContent.selectFirst("h1")
|
||||||
|
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
|
||||||
|
h1Element.remove()
|
||||||
|
}
|
||||||
|
callback(articleContent.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun queryRssXml(
|
||||||
|
rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
accountId: Int,
|
||||||
|
feed: Feed,
|
||||||
|
latestTitle: String? = null,
|
||||||
|
): List<Article> {
|
||||||
|
val a = mutableListOf<Article>()
|
||||||
|
try {
|
||||||
|
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
||||||
|
parseRss.items.forEach {
|
||||||
|
if (latestTitle != null && latestTitle == it.title) return a
|
||||||
|
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
|
||||||
|
a.add(
|
||||||
|
Article(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
accountId = accountId,
|
||||||
|
feedId = feed.id,
|
||||||
|
date = Date(it.publishDate.toString()),
|
||||||
|
title = it.title.toString(),
|
||||||
|
author = it.author,
|
||||||
|
rawDescription = it.description.toString(),
|
||||||
|
shortDescription = (Readability4JExtended("", it.description.toString())
|
||||||
|
.parse().textContent ?: "").trim().run {
|
||||||
|
if (this.length > 100) this.substring(0, 100)
|
||||||
|
else this
|
||||||
|
},
|
||||||
|
link = it.link ?: "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "error ${feed.name}: ${e.message}")
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun queryRssIcon(
|
||||||
|
feedDao: FeedDao,
|
||||||
|
feed: Feed,
|
||||||
|
articleLink: String?,
|
||||||
|
) {
|
||||||
|
if (articleLink == null) return
|
||||||
|
val execute = OkHttpClient()
|
||||||
|
.newCall(Request.Builder().url(articleLink).build())
|
||||||
|
.execute()
|
||||||
|
val content = execute.body?.string()
|
||||||
|
val regex =
|
||||||
|
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
|
||||||
|
if (content != null) {
|
||||||
|
var iconLink = regex
|
||||||
|
.find(content)
|
||||||
|
?.groups?.get(3)
|
||||||
|
?.value
|
||||||
|
Log.i("rlog", "queryRssIcon: $iconLink")
|
||||||
|
if (iconLink != null) {
|
||||||
|
if (iconLink.startsWith("//")) {
|
||||||
|
iconLink = "http:$iconLink"
|
||||||
|
}
|
||||||
|
if (iconLink.startsWith("/")) {
|
||||||
|
val domainRegex =
|
||||||
|
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
|
||||||
|
iconLink =
|
||||||
|
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
|
||||||
|
}
|
||||||
|
saveRssIcon(feedDao, feed, iconLink)
|
||||||
|
} else {
|
||||||
|
// saveRssIcon(feedDao, feed, "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// saveRssIcon(feedDao, feed, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
|
||||||
|
val execute = OkHttpClient()
|
||||||
|
.newCall(Request.Builder().url(iconLink).build())
|
||||||
|
.execute()
|
||||||
|
feedDao.update(
|
||||||
|
feed.apply {
|
||||||
|
icon = execute.body?.bytes()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,391 +1,27 @@
|
||||||
package me.ash.reader.data.repository
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
|
||||||
import androidx.work.*
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import me.ash.reader.DataStoreKeys
|
||||||
import kotlinx.coroutines.flow.*
|
import me.ash.reader.data.account.Account
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import me.ash.reader.dataStore
|
||||||
import kotlinx.coroutines.sync.withLock
|
import me.ash.reader.get
|
||||||
import me.ash.reader.*
|
|
||||||
import me.ash.reader.R
|
|
||||||
import me.ash.reader.data.account.AccountDao
|
|
||||||
import me.ash.reader.data.article.Article
|
|
||||||
import me.ash.reader.data.article.ArticleDao
|
|
||||||
import me.ash.reader.data.constant.Symbol
|
|
||||||
import me.ash.reader.data.feed.Feed
|
|
||||||
import me.ash.reader.data.feed.FeedDao
|
|
||||||
import me.ash.reader.data.feed.FeedWithArticle
|
|
||||||
import me.ash.reader.data.source.ReaderDatabase
|
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
|
||||||
import net.dankito.readability4j.Readability4J
|
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
|
||||||
import okhttp3.*
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
class RssRepository @Inject constructor(
|
class RssRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val accountDao: AccountDao,
|
private val localRssRepository: LocalRssRepository,
|
||||||
private val articleDao: ArticleDao,
|
// private val feverRssRepository: FeverRssRepository,
|
||||||
private val feedDao: FeedDao,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
|
||||||
private val workManager: WorkManager,
|
|
||||||
) {
|
) {
|
||||||
@Throws(Exception::class)
|
fun get() = when (getAccountType()) {
|
||||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
// Account.Type.LOCAL -> localRssRepository
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
Account.Type.LOCAL -> localRssRepository
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
// Account.Type.FEVER -> feverRssRepository
|
||||||
val feed = Feed(
|
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
||||||
name = parseRss.title!!,
|
else -> throw IllegalStateException("Unknown account type: ${getAccountType()}")
|
||||||
url = feedLink,
|
|
||||||
groupId = 0,
|
|
||||||
accountId = accountId,
|
|
||||||
)
|
|
||||||
val articles = mutableListOf<Article>()
|
|
||||||
parseRss.items.forEach {
|
|
||||||
articles.add(
|
|
||||||
Article(
|
|
||||||
accountId = accountId,
|
|
||||||
feedId = feed.id ?: 0,
|
|
||||||
date = Date(it.publishDate.toString()),
|
|
||||||
title = it.title.toString(),
|
|
||||||
author = it.author,
|
|
||||||
rawDescription = it.description.toString(),
|
|
||||||
shortDescription = (Readability4JExtended("", it.description.toString())
|
|
||||||
.parse().textContent ?: "").trim().run {
|
|
||||||
if (this.length > 100) this.substring(0, 100)
|
|
||||||
else this
|
|
||||||
},
|
|
||||||
link = it.link ?: "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return FeedWithArticle(feed, articles)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseDescriptionContent(link: String, content: String): String {
|
private fun getAccountType(): Int = context.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
||||||
val readability4J: Readability4J = Readability4JExtended(link, content)
|
|
||||||
val article = readability4J.parse()
|
|
||||||
val element = article.articleContent
|
|
||||||
return element.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseFullContent(link: String, title: String, callback: (String) -> Unit) {
|
|
||||||
OkHttpClient()
|
|
||||||
.newCall(Request.Builder().url(link).build())
|
|
||||||
.enqueue(object : Callback {
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
callback(e.message.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
val content = response.body?.string()
|
|
||||||
val readability4J: Readability4J =
|
|
||||||
Readability4JExtended(link, content ?: "")
|
|
||||||
val articleContent = readability4J.parse().articleContent
|
|
||||||
if (articleContent == null) {
|
|
||||||
callback("")
|
|
||||||
} else {
|
|
||||||
val h1Element = articleContent.selectFirst("h1")
|
|
||||||
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
|
|
||||||
h1Element.remove()
|
|
||||||
}
|
|
||||||
callback(articleContent.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun peekWork(): String {
|
|
||||||
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sync(isWork: Boolean? = false) {
|
|
||||||
if (isWork == true) {
|
|
||||||
workManager.cancelAllWork()
|
|
||||||
val syncWorkerRequest: WorkRequest =
|
|
||||||
PeriodicWorkRequestBuilder<SyncWorker>(
|
|
||||||
15, TimeUnit.MINUTES
|
|
||||||
).setConstraints(
|
|
||||||
Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
).addTag("sync").build()
|
|
||||||
workManager.enqueue(syncWorkerRequest)
|
|
||||||
} else {
|
|
||||||
normalSync(context, accountDao, articleDao, feedDao, rssNetworkDataSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
companion object {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
val syncState = MutableStateFlow(SyncState())
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
suspend fun normalSync(
|
|
||||||
context: Context,
|
|
||||||
accountDao: AccountDao,
|
|
||||||
articleDao: ArticleDao,
|
|
||||||
feedDao: FeedDao,
|
|
||||||
rssNetworkDataSource: RssNetworkDataSource
|
|
||||||
) {
|
|
||||||
doSync(context, accountDao, articleDao, feedDao, rssNetworkDataSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun workerSync(context: Context) {
|
|
||||||
val db = ReaderDatabase.getInstance(context)
|
|
||||||
doSync(
|
|
||||||
context,
|
|
||||||
db.accountDao(),
|
|
||||||
db.articleDao(),
|
|
||||||
db.feedDao(),
|
|
||||||
RssNetworkDataSource.getInstance()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun doSync(
|
|
||||||
context: Context,
|
|
||||||
accountDao: AccountDao,
|
|
||||||
articleDao: ArticleDao,
|
|
||||||
feedDao: FeedDao,
|
|
||||||
rssNetworkDataSource: RssNetworkDataSource
|
|
||||||
) {
|
|
||||||
mutex.withLock {
|
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
|
||||||
?: return
|
|
||||||
val feeds = feedDao.queryAll(accountId)
|
|
||||||
val feedNotificationMap = mutableMapOf<Int, Boolean>()
|
|
||||||
feeds.forEach { feed ->
|
|
||||||
feedNotificationMap[feed.id ?: 0] = feed.isNotification
|
|
||||||
}
|
|
||||||
val preTime = System.currentTimeMillis()
|
|
||||||
val chunked = feeds.chunked(6)
|
|
||||||
chunked.forEachIndexed { index, item ->
|
|
||||||
item.forEach {
|
|
||||||
Log.i("RlOG", "chunked $index: ${it.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val flows = mutableListOf<Flow<List<Article>>>()
|
|
||||||
repeat(chunked.size) {
|
|
||||||
flows.add(flow {
|
|
||||||
val articles = mutableListOf<Article>()
|
|
||||||
chunked[it].forEach { feed ->
|
|
||||||
val latest = articleDao.queryLatestByFeedId(accountId, feed.id ?: 0)
|
|
||||||
articles.addAll(
|
|
||||||
queryRssXml(
|
|
||||||
rssNetworkDataSource,
|
|
||||||
accountId,
|
|
||||||
feed,
|
|
||||||
latest?.title,
|
|
||||||
).also {
|
|
||||||
if (feed.icon == null && it.isNotEmpty()) {
|
|
||||||
queryRssIcon(feedDao, feed, it.first().link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
syncState.update {
|
|
||||||
it.copy(
|
|
||||||
feedCount = feeds.size,
|
|
||||||
syncedCount = syncState.value.syncedCount + 1,
|
|
||||||
currentFeedName = feed.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit(articles)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
combine(
|
|
||||||
flows
|
|
||||||
) {
|
|
||||||
val notificationManager: NotificationManager =
|
|
||||||
getSystemService(
|
|
||||||
context,
|
|
||||||
NotificationManager::class.java
|
|
||||||
) as NotificationManager
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
notificationManager.createNotificationChannel(
|
|
||||||
NotificationChannel(
|
|
||||||
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE,
|
|
||||||
"文章更新",
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
it.forEach { articleList ->
|
|
||||||
val ids = articleDao.insertList(articleList)
|
|
||||||
articleList.forEachIndexed { index, article ->
|
|
||||||
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
|
|
||||||
if (feedNotificationMap[article.feedId] == true) {
|
|
||||||
val builder = NotificationCompat.Builder(
|
|
||||||
context,
|
|
||||||
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
|
||||||
).setSmallIcon(R.drawable.ic_launcher_foreground)
|
|
||||||
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
|
||||||
.setContentTitle(article.title)
|
|
||||||
.setContentText(article.shortDescription)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setContentIntent(
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
ids[index].toInt(),
|
|
||||||
Intent(context, MainActivity::class.java).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
putExtra(
|
|
||||||
Symbol.EXTRA_ARTICLE_ID,
|
|
||||||
ids[index].toInt()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
notificationManager.notify(
|
|
||||||
ids[index].toInt(),
|
|
||||||
builder.build().apply {
|
|
||||||
flags = Notification.FLAG_AUTO_CANCEL
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.buffer().onCompletion {
|
|
||||||
val afterTime = System.currentTimeMillis()
|
|
||||||
Log.i("RlOG", "onCompletion: ${afterTime - preTime}")
|
|
||||||
accountDao.queryById(accountId)?.let { account ->
|
|
||||||
accountDao.update(
|
|
||||||
account.apply {
|
|
||||||
updateAt = Date()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
syncState.update {
|
|
||||||
it.copy(
|
|
||||||
feedCount = 0,
|
|
||||||
syncedCount = 0,
|
|
||||||
currentFeedName = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun queryRssXml(
|
|
||||||
rssNetworkDataSource: RssNetworkDataSource,
|
|
||||||
accountId: Int,
|
|
||||||
feed: Feed,
|
|
||||||
latestTitle: String? = null,
|
|
||||||
): List<Article> {
|
|
||||||
val a = mutableListOf<Article>()
|
|
||||||
try {
|
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
|
||||||
parseRss.items.forEach {
|
|
||||||
if (latestTitle != null && latestTitle == it.title) return a
|
|
||||||
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
|
|
||||||
a.add(
|
|
||||||
Article(
|
|
||||||
accountId = accountId,
|
|
||||||
feedId = feed.id ?: 0,
|
|
||||||
date = Date(it.publishDate.toString()),
|
|
||||||
title = it.title.toString(),
|
|
||||||
author = it.author,
|
|
||||||
rawDescription = it.description.toString(),
|
|
||||||
shortDescription = (Readability4JExtended("", it.description.toString())
|
|
||||||
.parse().textContent ?: "").trim().run {
|
|
||||||
if (this.length > 100) this.substring(0, 100)
|
|
||||||
else this
|
|
||||||
},
|
|
||||||
link = it.link ?: "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("RLog", "error ${feed.name}: ${e.message}")
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun queryRssIcon(
|
|
||||||
feedDao: FeedDao,
|
|
||||||
feed: Feed,
|
|
||||||
articleLink: String?,
|
|
||||||
) {
|
|
||||||
if (articleLink == null) return
|
|
||||||
val execute = OkHttpClient()
|
|
||||||
.newCall(Request.Builder().url(articleLink).build())
|
|
||||||
.execute()
|
|
||||||
val content = execute.body?.string()
|
|
||||||
val regex =
|
|
||||||
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
|
|
||||||
if (content != null) {
|
|
||||||
var iconLink = regex
|
|
||||||
.find(content)
|
|
||||||
?.groups?.get(3)
|
|
||||||
?.value
|
|
||||||
Log.i("rlog", "queryRssIcon: $iconLink")
|
|
||||||
if (iconLink != null) {
|
|
||||||
if (iconLink.startsWith("//")) {
|
|
||||||
iconLink = "http:$iconLink"
|
|
||||||
}
|
|
||||||
if (iconLink.startsWith("/")) {
|
|
||||||
val domainRegex =
|
|
||||||
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
|
|
||||||
iconLink =
|
|
||||||
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
|
|
||||||
}
|
|
||||||
saveRssIcon(feedDao, feed, iconLink)
|
|
||||||
} else {
|
|
||||||
// saveRssIcon(feedDao, feed, "")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// saveRssIcon(feedDao, feed, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
|
|
||||||
val execute = OkHttpClient()
|
|
||||||
.newCall(Request.Builder().url(iconLink).build())
|
|
||||||
.execute()
|
|
||||||
feedDao.update(
|
|
||||||
feed.apply {
|
|
||||||
icon = execute.body?.bytes()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
class SyncWorker(
|
|
||||||
context: Context,
|
|
||||||
workerParams: WorkerParameters,
|
|
||||||
) : CoroutineWorker(context, workerParams) {
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
Log.i("RLog", "doWork: ")
|
|
||||||
RssRepository.workerSync(applicationContext)
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
import com.github.muhrifqii.parserss.ParseRSS
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.Multipart
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Part
|
||||||
|
|
||||||
|
interface FeverApiDataSource {
|
||||||
|
@Multipart
|
||||||
|
@POST("fever.php?api&groups")
|
||||||
|
fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("fever.php?api&feeds")
|
||||||
|
fun feeds(@Part("api_key") apiKey: RequestBody?="1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: FeverApiDataSource? = null
|
||||||
|
|
||||||
|
fun getInstance(): FeverApiDataSource {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
ParseRSS.init(XmlPullParserFactory.newInstance())
|
||||||
|
instance ?: Retrofit.Builder()
|
||||||
|
.baseUrl("http://10.0.2.2/api/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build().create(FeverApiDataSource::class.java).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt
Normal file
54
app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
object FeverApiDto {
|
||||||
|
// &groups
|
||||||
|
data class Groups(
|
||||||
|
val apiVersion: Int,
|
||||||
|
val auth: Int,
|
||||||
|
val lastRefreshedOnTime: Long,
|
||||||
|
val groups: List<GroupItem>,
|
||||||
|
val feedsGroups: List<FeedsGroupsItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GroupItem(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeedsGroupsItem(
|
||||||
|
val groupId: Int,
|
||||||
|
val feedsIds: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// &feeds
|
||||||
|
data class Feed(
|
||||||
|
val apiVersion: Int,
|
||||||
|
val auth: Int,
|
||||||
|
val lastRefreshedOnTime: Long,
|
||||||
|
val feeds: List<FeedItem>,
|
||||||
|
val feedsGroups: List<FeedsGroupsItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeedItem(
|
||||||
|
val id: Int,
|
||||||
|
val favicon_id: Int,
|
||||||
|
val title: String,
|
||||||
|
val url: String,
|
||||||
|
val siteUrl: String,
|
||||||
|
val isSpark: Int,
|
||||||
|
val lastRefreshedOnTime: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
// &favicons
|
||||||
|
data class Favicons(
|
||||||
|
val apiVersion: Int,
|
||||||
|
val auth: Int,
|
||||||
|
val lastRefreshedOnTime: Long,
|
||||||
|
val favicons: List<FaviconItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FaviconItem(
|
||||||
|
val id: Int,
|
||||||
|
val data: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
import com.github.muhrifqii.parserss.ParseRSS
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
interface GoogleReaderApiDataSource {
|
||||||
|
@POST("accounts/ClientLogin")
|
||||||
|
fun login(Email: String, Passwd: String): Call<String>
|
||||||
|
|
||||||
|
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
||||||
|
@POST("reader/api/0/subscription/list?output=json")
|
||||||
|
fun subscriptionList(): Call<GoogleReaderApiDto.SubscriptionList>
|
||||||
|
|
||||||
|
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
||||||
|
@POST("reader/api/0/unread-count?output=json")
|
||||||
|
fun unreadCount(): Call<GoogleReaderApiDto.UnreadCount>
|
||||||
|
|
||||||
|
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
||||||
|
@POST("reader/api/0/tag/list?output=json")
|
||||||
|
fun tagList(): Call<GoogleReaderApiDto.TagList>
|
||||||
|
|
||||||
|
@Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}")
|
||||||
|
@POST("reader/api/0/stream/contents/reading-list")
|
||||||
|
fun readingList(): Call<GoogleReaderApiDto.ReadingList>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: GoogleReaderApiDataSource? = null
|
||||||
|
|
||||||
|
fun getInstance(): GoogleReaderApiDataSource {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
ParseRSS.init(XmlPullParserFactory.newInstance())
|
||||||
|
instance ?: Retrofit.Builder()
|
||||||
|
.baseUrl("http://10.0.2.2/api/greader.php/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build().create(GoogleReaderApiDataSource::class.java).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
|
object GoogleReaderApiDto {
|
||||||
|
// subscription/list?output=json
|
||||||
|
data class SubscriptionList(
|
||||||
|
val subscriptions: List<SubscriptionItem>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SubscriptionItem(
|
||||||
|
val id: String? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
val categories: List<CategoryItem>? = null,
|
||||||
|
val url: String? = null,
|
||||||
|
val htmlUrl: String? = null,
|
||||||
|
val iconUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CategoryItem(
|
||||||
|
val id: String? = null,
|
||||||
|
val label: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
// unread-count?output=json
|
||||||
|
data class UnreadCount(
|
||||||
|
val max: Int? = null,
|
||||||
|
val unreadcounts: List<UnreadCountItem>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UnreadCountItem(
|
||||||
|
val id: String? = null,
|
||||||
|
val count: Int? = null,
|
||||||
|
val newestItemTimestampUsec: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
// tag/list?output=json
|
||||||
|
data class TagList(
|
||||||
|
val tags: List<TagItem>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TagItem(
|
||||||
|
val id: String? = null,
|
||||||
|
val type: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
// stream/contents/reading-list?output=json
|
||||||
|
data class ReadingList(
|
||||||
|
val id: String? = null,
|
||||||
|
val updated: Long? = null,
|
||||||
|
val items: List<Item>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Item(
|
||||||
|
val id: String? = null,
|
||||||
|
val crawlTimeMsec: String? = null,
|
||||||
|
val timestampUsec: String? = null,
|
||||||
|
val published: Long? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
val summary: Summary? = null,
|
||||||
|
val categories: List<String>? = null,
|
||||||
|
val origin: List<OriginItem>? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Summary(
|
||||||
|
val content: String? = null,
|
||||||
|
val canonical: List<CanonicalItem>? = null,
|
||||||
|
val alternate: List<CanonicalItem>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CanonicalItem(
|
||||||
|
val href: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OriginItem(
|
||||||
|
val streamId: String? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
)
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class OpmlLocalDataSource @Inject constructor(
|
class OpmlLocalDataSource @Inject constructor(
|
||||||
|
@ -42,9 +43,10 @@ class OpmlLocalDataSource @Inject constructor(
|
||||||
Log.i("RLog", "rss: ${title} , ${xmlUrl}")
|
Log.i("RLog", "rss: ${title} , ${xmlUrl}")
|
||||||
groupWithFeedList.last().feeds.add(
|
groupWithFeedList.last().feeds.add(
|
||||||
Feed(
|
Feed(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
name = title,
|
name = title,
|
||||||
url = xmlUrl,
|
url = xmlUrl,
|
||||||
groupId = 0,
|
groupId = UUID.randomUUID().toString(),
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -54,6 +56,7 @@ class OpmlLocalDataSource @Inject constructor(
|
||||||
groupWithFeedList.add(
|
groupWithFeedList.add(
|
||||||
GroupWithFeed(
|
GroupWithFeed(
|
||||||
group = Group(
|
group = Group(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
name = title,
|
name = title,
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
),
|
),
|
||||||
|
|
|
@ -46,7 +46,7 @@ fun HomePage(
|
||||||
readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
|
readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val article =
|
val article =
|
||||||
readViewModel.articleRepository.findArticleById(it.toString().toInt())
|
readViewModel.rssRepository.get().findArticleById(it.toString().toInt())
|
||||||
?: return@launch
|
?: return@launch
|
||||||
readViewModel.dispatch(ReadViewAction.InitData(article))
|
readViewModel.dispatch(ReadViewAction.InitData(article))
|
||||||
if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
|
if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
|
||||||
|
|
|
@ -31,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 = RssRepository.syncState
|
val syncState = rssRepository.get().syncState
|
||||||
|
|
||||||
fun dispatch(action: HomeViewAction) {
|
fun dispatch(action: HomeViewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
@ -47,7 +47,7 @@ class HomeViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun sync(callback: () -> Unit = {}) {
|
private fun sync(callback: () -> Unit = {}) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
rssRepository.sync()
|
rssRepository.get().doSync()
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,11 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.article.ArticleWithFeed
|
import me.ash.reader.data.article.ArticleWithFeed
|
||||||
import me.ash.reader.data.repository.ArticleRepository
|
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ArticleViewModel @Inject constructor(
|
class ArticleViewModel @Inject constructor(
|
||||||
private val articleRepository: ArticleRepository,
|
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _viewState = MutableStateFlow(ArticleViewState())
|
private val _viewState = MutableStateFlow(ArticleViewState())
|
||||||
|
@ -41,19 +39,19 @@ class ArticleViewModel @Inject constructor(
|
||||||
private fun peekSyncWork() {
|
private fun peekSyncWork() {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
syncWorkInfo = rssRepository.peekWork()
|
syncWorkInfo = rssRepository.get().peekWork()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchData(
|
private fun fetchData(
|
||||||
groupId: Int? = null,
|
groupId: String? = null,
|
||||||
feedId: Int? = null,
|
feedId: String? = null,
|
||||||
isStarred: Boolean,
|
isStarred: Boolean,
|
||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
articleRepository.pullImportant(isStarred, true)
|
rssRepository.get().pullImportant(isStarred, true)
|
||||||
.collect { importantList ->
|
.collect { importantList ->
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -65,7 +63,7 @@ class ArticleViewModel @Inject constructor(
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
||||||
articleRepository.pullArticles(
|
rssRepository.get().pullArticles(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
feedId = feedId,
|
feedId = feedId,
|
||||||
isStarred = isStarred,
|
isStarred = isStarred,
|
||||||
|
@ -99,8 +97,8 @@ data class ArticleViewState(
|
||||||
|
|
||||||
sealed class ArticleViewAction {
|
sealed class ArticleViewAction {
|
||||||
data class FetchData(
|
data class FetchData(
|
||||||
val groupId: Int? = null,
|
val groupId: String? = null,
|
||||||
val feedId: Int? = null,
|
val feedId: String? = null,
|
||||||
val isStarred: Boolean,
|
val isStarred: Boolean,
|
||||||
val isUnread: Boolean,
|
val isUnread: Boolean,
|
||||||
) : ArticleViewAction()
|
) : ArticleViewAction()
|
||||||
|
|
|
@ -11,15 +11,15 @@ import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.account.Account
|
import me.ash.reader.data.account.Account
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
import me.ash.reader.data.repository.AccountRepository
|
import me.ash.reader.data.repository.AccountRepository
|
||||||
import me.ash.reader.data.repository.ArticleRepository
|
|
||||||
import me.ash.reader.data.repository.OpmlRepository
|
import me.ash.reader.data.repository.OpmlRepository
|
||||||
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FeedViewModel @Inject constructor(
|
class FeedViewModel @Inject constructor(
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
private val articleRepository: ArticleRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlRepository: OpmlRepository,
|
private val opmlRepository: OpmlRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _viewState = MutableStateFlow(FeedViewState())
|
private val _viewState = MutableStateFlow(FeedViewState())
|
||||||
|
@ -62,11 +62,11 @@ class FeedViewModel @Inject constructor(
|
||||||
|
|
||||||
private suspend fun pullFeeds(isStarred: Boolean, isUnread: Boolean) {
|
private suspend fun pullFeeds(isStarred: Boolean, isUnread: Boolean) {
|
||||||
combine(
|
combine(
|
||||||
articleRepository.pullFeeds(),
|
rssRepository.get().pullFeeds(),
|
||||||
articleRepository.pullImportant(isStarred, isUnread),
|
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||||
) { groupWithFeedList, importantList ->
|
) { groupWithFeedList, importantList ->
|
||||||
val groupImportantMap = mutableMapOf<Int, Int>()
|
val groupImportantMap = mutableMapOf<String, Int>()
|
||||||
val feedImportantMap = mutableMapOf<Int, Int>()
|
val feedImportantMap = mutableMapOf<String, Int>()
|
||||||
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
||||||
var groupImportantSum = 0
|
var groupImportantSum = 0
|
||||||
list.forEach {
|
list.forEach {
|
||||||
|
|
|
@ -28,10 +28,10 @@ fun ResultViewPage(
|
||||||
groups: List<Group> = emptyList(),
|
groups: List<Group> = emptyList(),
|
||||||
selectedNotificationPreset: Boolean = false,
|
selectedNotificationPreset: Boolean = false,
|
||||||
selectedFullContentParsePreset: Boolean = false,
|
selectedFullContentParsePreset: Boolean = false,
|
||||||
selectedGroupId: Int = 0,
|
selectedGroupId: String = "",
|
||||||
notificationPresetOnClick: () -> Unit = {},
|
notificationPresetOnClick: () -> Unit = {},
|
||||||
fullContentParsePresetOnClick: () -> Unit = {},
|
fullContentParsePresetOnClick: () -> Unit = {},
|
||||||
groupOnClick: (groupId: Int) -> Unit = {},
|
groupOnClick: (groupId: String) -> Unit = {},
|
||||||
onKeyboardAction: () -> Unit = {},
|
onKeyboardAction: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
@ -133,8 +133,8 @@ private fun Preset(
|
||||||
@Composable
|
@Composable
|
||||||
private fun AddToGroup(
|
private fun AddToGroup(
|
||||||
groups: List<Group>,
|
groups: List<Group>,
|
||||||
selectedGroupId: Int,
|
selectedGroupId: String,
|
||||||
groupOnClick: (groupId: Int) -> Unit = {},
|
groupOnClick: (groupId: String) -> Unit = {},
|
||||||
onKeyboardAction: () -> Unit = {},
|
onKeyboardAction: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -152,7 +152,7 @@ private fun AddToGroup(
|
||||||
SelectionChip(
|
SelectionChip(
|
||||||
modifier = Modifier.animateContentSize(),
|
modifier = Modifier.animateContentSize(),
|
||||||
selected = it.id == selectedGroupId,
|
selected = it.id == selectedGroupId,
|
||||||
onClick = { groupOnClick(it.id ?: 0) },
|
onClick = { groupOnClick(it.id) },
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = it.name,
|
text = it.name,
|
||||||
|
|
|
@ -82,7 +82,7 @@ fun SubscribeDialog(
|
||||||
groups = groupsState.value,
|
groups = groupsState.value,
|
||||||
selectedNotificationPreset = viewState.notificationPreset,
|
selectedNotificationPreset = viewState.notificationPreset,
|
||||||
selectedFullContentParsePreset = viewState.fullContentParsePreset,
|
selectedFullContentParsePreset = viewState.fullContentParsePreset,
|
||||||
selectedGroupId = viewState.selectedGroupId ?: 0,
|
selectedGroupId = viewState.selectedGroupId,
|
||||||
pagerState = viewState.pagerState,
|
pagerState = viewState.pagerState,
|
||||||
notificationPresetOnClick = {
|
notificationPresetOnClick = {
|
||||||
viewModel.dispatch(SubscribeViewAction.ChangeNotificationPreset)
|
viewModel.dispatch(SubscribeViewAction.ChangeNotificationPreset)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.constant.Symbol
|
import me.ash.reader.data.constant.Symbol
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.repository.ArticleRepository
|
import me.ash.reader.data.repository.RssHelper
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import me.ash.reader.formatUrl
|
import me.ash.reader.formatUrl
|
||||||
import me.ash.reader.ui.extension.animateScrollToPage
|
import me.ash.reader.ui.extension.animateScrollToPage
|
||||||
|
@ -22,8 +22,8 @@ import javax.inject.Inject
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SubscribeViewModel @Inject constructor(
|
class SubscribeViewModel @Inject constructor(
|
||||||
private val articleRepository: ArticleRepository,
|
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
|
private val rssHelper: RssHelper,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _viewState = MutableStateFlow(SubScribeViewState())
|
private val _viewState = MutableStateFlow(SubScribeViewState())
|
||||||
val viewState: StateFlow<SubScribeViewState> = _viewState.asStateFlow()
|
val viewState: StateFlow<SubScribeViewState> = _viewState.asStateFlow()
|
||||||
|
@ -48,7 +48,7 @@ class SubscribeViewModel @Inject constructor(
|
||||||
private fun init() {
|
private fun init() {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
groups = articleRepository.pullGroups()
|
groups = rssRepository.get().pullGroups()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ class SubscribeViewModel @Inject constructor(
|
||||||
articles = emptyList(),
|
articles = emptyList(),
|
||||||
notificationPreset = false,
|
notificationPreset = false,
|
||||||
fullContentParsePreset = false,
|
fullContentParsePreset = false,
|
||||||
selectedGroupId = null,
|
selectedGroupId = "",
|
||||||
groups = emptyFlow(),
|
groups = emptyFlow(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -73,9 +73,9 @@ class SubscribeViewModel @Inject constructor(
|
||||||
private fun subscribe() {
|
private fun subscribe() {
|
||||||
val feed = _viewState.value.feed ?: return
|
val feed = _viewState.value.feed ?: return
|
||||||
val articles = _viewState.value.articles
|
val articles = _viewState.value.articles
|
||||||
val groupId = _viewState.value.selectedGroupId ?: 0
|
val groupId = _viewState.value.selectedGroupId
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
articleRepository.subscribe(
|
rssRepository.get().subscribe(
|
||||||
feed.copy(
|
feed.copy(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
isNotification = _viewState.value.notificationPreset,
|
isNotification = _viewState.value.notificationPreset,
|
||||||
|
@ -86,7 +86,7 @@ class SubscribeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectedGroup(groupId: Int? = null) {
|
private fun selectedGroup(groupId: String) {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedGroupId = groupId,
|
selectedGroupId = groupId,
|
||||||
|
@ -127,7 +127,7 @@ class SubscribeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val feedWithArticle = rssRepository.searchFeed(_viewState.value.inputContent)
|
val feedWithArticle = rssHelper.searchFeed(_viewState.value.inputContent)
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
feed = feedWithArticle.feed,
|
feed = feedWithArticle.feed,
|
||||||
|
@ -174,7 +174,7 @@ data class SubScribeViewState(
|
||||||
val articles: List<Article> = emptyList(),
|
val articles: List<Article> = emptyList(),
|
||||||
val notificationPreset: Boolean = false,
|
val notificationPreset: Boolean = false,
|
||||||
val fullContentParsePreset: Boolean = false,
|
val fullContentParsePreset: Boolean = false,
|
||||||
val selectedGroupId: Int? = null,
|
val selectedGroupId: String = "",
|
||||||
val groups: Flow<List<Group>> = emptyFlow(),
|
val groups: Flow<List<Group>> = emptyFlow(),
|
||||||
val pagerState: PagerState = PagerState(),
|
val pagerState: PagerState = PagerState(),
|
||||||
)
|
)
|
||||||
|
@ -198,7 +198,7 @@ sealed class SubscribeViewAction {
|
||||||
object ChangeFullContentParsePreset : SubscribeViewAction()
|
object ChangeFullContentParsePreset : SubscribeViewAction()
|
||||||
|
|
||||||
data class SelectedGroup(
|
data class SelectedGroup(
|
||||||
val groupId: Int? = null
|
val groupId: String
|
||||||
) : SubscribeViewAction()
|
) : SubscribeViewAction()
|
||||||
|
|
||||||
object Subscribe : SubscribeViewAction()
|
object Subscribe : SubscribeViewAction()
|
||||||
|
|
|
@ -21,11 +21,11 @@ fun SubscribeViewPager(
|
||||||
groups: List<Group> = emptyList(),
|
groups: List<Group> = emptyList(),
|
||||||
selectedNotificationPreset: Boolean = false,
|
selectedNotificationPreset: Boolean = false,
|
||||||
selectedFullContentParsePreset: Boolean = false,
|
selectedFullContentParsePreset: Boolean = false,
|
||||||
selectedGroupId: Int = 0,
|
selectedGroupId: String = "",
|
||||||
pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
||||||
notificationPresetOnClick: () -> Unit = {},
|
notificationPresetOnClick: () -> Unit = {},
|
||||||
fullContentParsePresetOnClick: () -> Unit = {},
|
fullContentParsePresetOnClick: () -> Unit = {},
|
||||||
groupOnClick: (groupId: Int) -> Unit = {},
|
groupOnClick: (groupId: String) -> Unit = {},
|
||||||
onResultKeyboardAction: () -> Unit = {},
|
onResultKeyboardAction: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
ViewPager(
|
ViewPager(
|
||||||
|
|
|
@ -10,14 +10,14 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.article.ArticleWithFeed
|
import me.ash.reader.data.article.ArticleWithFeed
|
||||||
import me.ash.reader.data.repository.ArticleRepository
|
import me.ash.reader.data.repository.RssHelper
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ReadViewModel @Inject constructor(
|
class ReadViewModel @Inject constructor(
|
||||||
val articleRepository: ArticleRepository,
|
val rssRepository: RssRepository,
|
||||||
private val rssRepository: RssRepository,
|
private val rssHelper: RssHelper,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _viewState = MutableStateFlow(ReadViewState())
|
private val _viewState = MutableStateFlow(ReadViewState())
|
||||||
val viewState: StateFlow<ReadViewState> = _viewState.asStateFlow()
|
val viewState: StateFlow<ReadViewState> = _viewState.asStateFlow()
|
||||||
|
@ -44,7 +44,7 @@ class ReadViewModel @Inject constructor(
|
||||||
private fun renderDescriptionContent() {
|
private fun renderDescriptionContent() {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
content = rssRepository.parseDescriptionContent(
|
content = rssHelper.parseDescriptionContent(
|
||||||
link = it.articleWithFeed?.article?.link ?: "",
|
link = it.articleWithFeed?.article?.link ?: "",
|
||||||
content = it.articleWithFeed?.article?.rawDescription ?: "",
|
content = it.articleWithFeed?.article?.rawDescription ?: "",
|
||||||
)
|
)
|
||||||
|
@ -54,7 +54,7 @@ class ReadViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun renderFullContent() {
|
private fun renderFullContent() {
|
||||||
changeLoading(true)
|
changeLoading(true)
|
||||||
rssRepository.parseFullContent(
|
rssHelper.parseFullContent(
|
||||||
_viewState.value.articleWithFeed?.article?.link ?: "",
|
_viewState.value.articleWithFeed?.article?.link ?: "",
|
||||||
_viewState.value.articleWithFeed?.article?.title ?: ""
|
_viewState.value.articleWithFeed?.article?.title ?: ""
|
||||||
) { content ->
|
) { content ->
|
||||||
|
@ -76,7 +76,7 @@ class ReadViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
articleRepository.updateArticleInfo(
|
rssRepository.get().updateArticleInfo(
|
||||||
it.article.copy(
|
it.article.copy(
|
||||||
isUnread = isUnread
|
isUnread = isUnread
|
||||||
)
|
)
|
||||||
|
@ -97,7 +97,7 @@ class ReadViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
articleRepository.updateArticleInfo(
|
rssRepository.get().updateArticleInfo(
|
||||||
it.article.copy(
|
it.article.copy(
|
||||||
isStarred = isStarred
|
isStarred = isStarred
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user