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.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
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.RssRepository
|
||||
import me.ash.reader.data.repository.*
|
||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||
import me.ash.reader.data.source.ReaderDatabase
|
||||
import me.ash.reader.data.source.RssNetworkDataSource
|
||||
|
@ -26,11 +23,14 @@ class App : Application() {
|
|||
@Inject
|
||||
lateinit var rssNetworkDataSource: RssNetworkDataSource
|
||||
|
||||
@Inject
|
||||
lateinit var rssHelper: RssHelper
|
||||
|
||||
@Inject
|
||||
lateinit var accountRepository: AccountRepository
|
||||
|
||||
@Inject
|
||||
lateinit var articleRepository: ArticleRepository
|
||||
lateinit var localRssRepository: LocalRssRepository
|
||||
|
||||
@Inject
|
||||
lateinit var opmlRepository: OpmlRepository
|
||||
|
@ -42,10 +42,11 @@ class App : Application() {
|
|||
super.onCreate()
|
||||
GlobalScope.launch {
|
||||
if (accountRepository.isNoAccount()) {
|
||||
val accountId = accountRepository.addDefaultAccount()
|
||||
applicationContext.dataStore.put(DataStoreKeys.CurrentAccountId, accountId)
|
||||
val account = accountRepository.addDefaultAccount()
|
||||
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>
|
||||
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")
|
||||
data class Account(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int? = null,
|
||||
var id: Int? = null,
|
||||
@ColumnInfo
|
||||
var name: String,
|
||||
@ColumnInfo
|
||||
|
@ -18,6 +18,7 @@ data class Account(
|
|||
) {
|
||||
object Type {
|
||||
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(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int? = null,
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
@ColumnInfo
|
||||
val date: Date,
|
||||
@ColumnInfo
|
||||
|
@ -35,7 +35,7 @@ data class Article(
|
|||
@ColumnInfo
|
||||
val link: String,
|
||||
@ColumnInfo(index = true)
|
||||
val feedId: Int,
|
||||
val feedId: String,
|
||||
@ColumnInfo(index = true)
|
||||
val accountId: Int,
|
||||
@ColumnInfo(defaultValue = "true")
|
||||
|
|
|
@ -167,7 +167,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByGroupIdWhenIsAll(
|
||||
accountId: Int,
|
||||
groupId: Int
|
||||
groupId: String,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
@Transaction
|
||||
|
@ -188,7 +188,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByGroupIdWhenIsStarred(
|
||||
accountId: Int,
|
||||
groupId: Int,
|
||||
groupId: String,
|
||||
isStarred: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
|
@ -210,7 +210,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByGroupIdWhenIsUnread(
|
||||
accountId: Int,
|
||||
groupId: Int,
|
||||
groupId: String,
|
||||
isUnread: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
|
@ -224,7 +224,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByFeedIdWhenIsAll(
|
||||
accountId: Int,
|
||||
feedId: Int
|
||||
feedId: String
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
@Transaction
|
||||
|
@ -238,7 +238,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByFeedIdWhenIsStarred(
|
||||
accountId: Int,
|
||||
feedId: Int,
|
||||
feedId: String,
|
||||
isStarred: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
|
@ -253,7 +253,7 @@ interface ArticleDao {
|
|||
)
|
||||
fun queryArticleWithFeedByFeedIdWhenIsUnread(
|
||||
accountId: Int,
|
||||
feedId: Int,
|
||||
feedId: String,
|
||||
isUnread: Boolean,
|
||||
): PagingSource<Int, ArticleWithFeed>
|
||||
|
||||
|
@ -270,7 +270,7 @@ interface ArticleDao {
|
|||
ORDER BY date DESC LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun queryLatestByFeedId(accountId: Int, feedId: Int): Article?
|
||||
suspend fun queryLatestByFeedId(accountId: Int, feedId: String): Article?
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
|
|
|
@ -2,6 +2,6 @@ package me.ash.reader.data.article
|
|||
|
||||
data class ImportantCount(
|
||||
val important: Int,
|
||||
val feedId: Int,
|
||||
val groupId: Int,
|
||||
val feedId: String,
|
||||
val groupId: String,
|
||||
)
|
||||
|
|
|
@ -14,8 +14,8 @@ import me.ash.reader.data.group.Group
|
|||
)],
|
||||
)
|
||||
data class Feed(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int? = null,
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
@ColumnInfo
|
||||
val name: String,
|
||||
@ColumnInfo
|
||||
|
@ -23,7 +23,7 @@ data class Feed(
|
|||
@ColumnInfo
|
||||
val url: String,
|
||||
@ColumnInfo(index = true)
|
||||
var groupId: Int? = null,
|
||||
var groupId: String,
|
||||
@ColumnInfo(index = true)
|
||||
val accountId: Int,
|
||||
@ColumnInfo(defaultValue = "false")
|
||||
|
@ -33,7 +33,6 @@ data class Feed(
|
|||
) {
|
||||
@Ignore
|
||||
var important: Int? = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
@ -57,11 +56,11 @@ data class Feed(
|
|||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + (icon?.contentHashCode() ?: 0)
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + (groupId ?: 0)
|
||||
result = 31 * result + groupId.hashCode()
|
||||
result = 31 * result + accountId
|
||||
result = 31 * result + isNotification.hashCode()
|
||||
result = 31 * result + isFullContent.hashCode()
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.room.PrimaryKey
|
|||
|
||||
@Entity(tableName = "group")
|
||||
data class Group(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int? = null,
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
@ColumnInfo
|
||||
val name: String,
|
||||
@ColumnInfo(index = true)
|
||||
|
|
|
@ -4,14 +4,26 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
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 javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class RssNetworkModule {
|
||||
class RetrofitModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideRssNetworkDataSource(): RssNetworkDataSource =
|
||||
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.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import androidx.work.*
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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.ArticleDao
|
||||
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.GroupDao
|
||||
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.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class ArticleRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
abstract class AbstractRssRepository constructor(
|
||||
private val context: Context,
|
||||
private val accountDao: AccountDao,
|
||||
private val articleDao: ArticleDao,
|
||||
private val groupDao: GroupDao,
|
||||
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>> {
|
||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
||||
return groupDao.queryAllGroup(accountId)
|
||||
|
@ -38,8 +66,8 @@ class ArticleRepository @Inject constructor(
|
|||
}
|
||||
|
||||
fun pullArticles(
|
||||
groupId: Int? = null,
|
||||
feedId: Int? = null,
|
||||
groupId: String? = null,
|
||||
feedId: String? = null,
|
||||
isStarred: Boolean = false,
|
||||
isUnread: Boolean = false,
|
||||
): 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? {
|
||||
return articleDao.queryById(id)
|
||||
}
|
||||
|
||||
suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
||||
val feedId = feedDao.insert(feed).toInt()
|
||||
articleDao.insertList(articles.map {
|
||||
it.copy(feedId = feedId)
|
||||
})
|
||||
fun peekWork(): String {
|
||||
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
suspend fun addDefaultAccount(): Int {
|
||||
return accountDao.insert(
|
||||
Account(
|
||||
name = "Feeds",
|
||||
suspend fun addDefaultAccount(): Account {
|
||||
return Account(
|
||||
name = "Reader You",
|
||||
type = Account.Type.LOCAL,
|
||||
)
|
||||
).toInt()
|
||||
).apply {
|
||||
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 {
|
||||
val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream)
|
||||
groupWithFeedList.forEach { groupWithFeed ->
|
||||
val id = groupDao.insert(groupWithFeed.group).toInt()
|
||||
groupWithFeed.feeds.forEach { it.groupId = id }
|
||||
groupDao.insert(groupWithFeed.group)
|
||||
groupWithFeed.feeds.forEach { it.groupId = groupWithFeed.group.id }
|
||||
feedDao.insertList(groupWithFeed.feeds)
|
||||
}
|
||||
} 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
|
||||
|
||||
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.*
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
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 me.ash.reader.DataStoreKeys
|
||||
import me.ash.reader.data.account.Account
|
||||
import me.ash.reader.dataStore
|
||||
import me.ash.reader.get
|
||||
import javax.inject.Inject
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
class RssRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val accountDao: AccountDao,
|
||||
private val articleDao: ArticleDao,
|
||||
private val feedDao: FeedDao,
|
||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||
private val workManager: WorkManager,
|
||||
private val localRssRepository: LocalRssRepository,
|
||||
// private val feverRssRepository: FeverRssRepository,
|
||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||
) {
|
||||
@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(
|
||||
name = parseRss.title!!,
|
||||
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 get() = when (getAccountType()) {
|
||||
// Account.Type.LOCAL -> localRssRepository
|
||||
Account.Type.LOCAL -> localRssRepository
|
||||
// Account.Type.FEVER -> feverRssRepository
|
||||
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
||||
else -> throw IllegalStateException("Unknown account type: ${getAccountType()}")
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
private fun getAccountType(): Int = context.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
||||
}
|
|
@ -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 java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class OpmlLocalDataSource @Inject constructor(
|
||||
|
@ -42,9 +43,10 @@ class OpmlLocalDataSource @Inject constructor(
|
|||
Log.i("RLog", "rss: ${title} , ${xmlUrl}")
|
||||
groupWithFeedList.last().feeds.add(
|
||||
Feed(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = title,
|
||||
url = xmlUrl,
|
||||
groupId = 0,
|
||||
groupId = UUID.randomUUID().toString(),
|
||||
accountId = accountId,
|
||||
)
|
||||
)
|
||||
|
@ -54,6 +56,7 @@ class OpmlLocalDataSource @Inject constructor(
|
|||
groupWithFeedList.add(
|
||||
GroupWithFeed(
|
||||
group = Group(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = title,
|
||||
accountId = accountId,
|
||||
),
|
||||
|
|
|
@ -46,7 +46,7 @@ fun HomePage(
|
|||
readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
|
||||
scope.launch {
|
||||
val article =
|
||||
readViewModel.articleRepository.findArticleById(it.toString().toInt())
|
||||
readViewModel.rssRepository.get().findArticleById(it.toString().toInt())
|
||||
?: return@launch
|
||||
readViewModel.dispatch(ReadViewAction.InitData(article))
|
||||
if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
|
||||
|
|
|
@ -31,7 +31,7 @@ class HomeViewModel @Inject constructor(
|
|||
private val _filterState = MutableStateFlow(FilterState())
|
||||
val filterState = _filterState.asStateFlow()
|
||||
|
||||
val syncState = RssRepository.syncState
|
||||
val syncState = rssRepository.get().syncState
|
||||
|
||||
fun dispatch(action: HomeViewAction) {
|
||||
when (action) {
|
||||
|
@ -47,7 +47,7 @@ class HomeViewModel @Inject constructor(
|
|||
|
||||
private fun sync(callback: () -> Unit = {}) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
rssRepository.sync()
|
||||
rssRepository.get().doSync()
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,11 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.article.ArticleWithFeed
|
||||
import me.ash.reader.data.repository.ArticleRepository
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ArticleViewModel @Inject constructor(
|
||||
private val articleRepository: ArticleRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
) : ViewModel() {
|
||||
private val _viewState = MutableStateFlow(ArticleViewState())
|
||||
|
@ -41,19 +39,19 @@ class ArticleViewModel @Inject constructor(
|
|||
private fun peekSyncWork() {
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
syncWorkInfo = rssRepository.peekWork()
|
||||
syncWorkInfo = rssRepository.get().peekWork()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchData(
|
||||
groupId: Int? = null,
|
||||
feedId: Int? = null,
|
||||
groupId: String? = null,
|
||||
feedId: String? = null,
|
||||
isStarred: Boolean,
|
||||
isUnread: Boolean,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
articleRepository.pullImportant(isStarred, true)
|
||||
rssRepository.get().pullImportant(isStarred, true)
|
||||
.collect { importantList ->
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
|
@ -65,7 +63,7 @@ class ArticleViewModel @Inject constructor(
|
|||
_viewState.update {
|
||||
it.copy(
|
||||
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
||||
articleRepository.pullArticles(
|
||||
rssRepository.get().pullArticles(
|
||||
groupId = groupId,
|
||||
feedId = feedId,
|
||||
isStarred = isStarred,
|
||||
|
@ -99,8 +97,8 @@ data class ArticleViewState(
|
|||
|
||||
sealed class ArticleViewAction {
|
||||
data class FetchData(
|
||||
val groupId: Int? = null,
|
||||
val feedId: Int? = null,
|
||||
val groupId: String? = null,
|
||||
val feedId: String? = null,
|
||||
val isStarred: Boolean,
|
||||
val isUnread: Boolean,
|
||||
) : ArticleViewAction()
|
||||
|
|
|
@ -11,15 +11,15 @@ import kotlinx.coroutines.launch
|
|||
import me.ash.reader.data.account.Account
|
||||
import me.ash.reader.data.group.GroupWithFeed
|
||||
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.RssRepository
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FeedViewModel @Inject constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val articleRepository: ArticleRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
private val opmlRepository: OpmlRepository,
|
||||
) : ViewModel() {
|
||||
private val _viewState = MutableStateFlow(FeedViewState())
|
||||
|
@ -62,11 +62,11 @@ class FeedViewModel @Inject constructor(
|
|||
|
||||
private suspend fun pullFeeds(isStarred: Boolean, isUnread: Boolean) {
|
||||
combine(
|
||||
articleRepository.pullFeeds(),
|
||||
articleRepository.pullImportant(isStarred, isUnread),
|
||||
rssRepository.get().pullFeeds(),
|
||||
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||
) { groupWithFeedList, importantList ->
|
||||
val groupImportantMap = mutableMapOf<Int, Int>()
|
||||
val feedImportantMap = mutableMapOf<Int, Int>()
|
||||
val groupImportantMap = mutableMapOf<String, Int>()
|
||||
val feedImportantMap = mutableMapOf<String, Int>()
|
||||
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
||||
var groupImportantSum = 0
|
||||
list.forEach {
|
||||
|
|
|
@ -28,10 +28,10 @@ fun ResultViewPage(
|
|||
groups: List<Group> = emptyList(),
|
||||
selectedNotificationPreset: Boolean = false,
|
||||
selectedFullContentParsePreset: Boolean = false,
|
||||
selectedGroupId: Int = 0,
|
||||
selectedGroupId: String = "",
|
||||
notificationPresetOnClick: () -> Unit = {},
|
||||
fullContentParsePresetOnClick: () -> Unit = {},
|
||||
groupOnClick: (groupId: Int) -> Unit = {},
|
||||
groupOnClick: (groupId: String) -> Unit = {},
|
||||
onKeyboardAction: () -> Unit = {},
|
||||
) {
|
||||
Column {
|
||||
|
@ -133,8 +133,8 @@ private fun Preset(
|
|||
@Composable
|
||||
private fun AddToGroup(
|
||||
groups: List<Group>,
|
||||
selectedGroupId: Int,
|
||||
groupOnClick: (groupId: Int) -> Unit = {},
|
||||
selectedGroupId: String,
|
||||
groupOnClick: (groupId: String) -> Unit = {},
|
||||
onKeyboardAction: () -> Unit = {},
|
||||
) {
|
||||
Text(
|
||||
|
@ -152,7 +152,7 @@ private fun AddToGroup(
|
|||
SelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
selected = it.id == selectedGroupId,
|
||||
onClick = { groupOnClick(it.id ?: 0) },
|
||||
onClick = { groupOnClick(it.id) },
|
||||
) {
|
||||
Text(
|
||||
text = it.name,
|
||||
|
|
|
@ -82,7 +82,7 @@ fun SubscribeDialog(
|
|||
groups = groupsState.value,
|
||||
selectedNotificationPreset = viewState.notificationPreset,
|
||||
selectedFullContentParsePreset = viewState.fullContentParsePreset,
|
||||
selectedGroupId = viewState.selectedGroupId ?: 0,
|
||||
selectedGroupId = viewState.selectedGroupId,
|
||||
pagerState = viewState.pagerState,
|
||||
notificationPresetOnClick = {
|
||||
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.feed.Feed
|
||||
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.formatUrl
|
||||
import me.ash.reader.ui.extension.animateScrollToPage
|
||||
|
@ -22,8 +22,8 @@ import javax.inject.Inject
|
|||
@OptIn(ExperimentalPagerApi::class)
|
||||
@HiltViewModel
|
||||
class SubscribeViewModel @Inject constructor(
|
||||
private val articleRepository: ArticleRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
private val rssHelper: RssHelper,
|
||||
) : ViewModel() {
|
||||
private val _viewState = MutableStateFlow(SubScribeViewState())
|
||||
val viewState: StateFlow<SubScribeViewState> = _viewState.asStateFlow()
|
||||
|
@ -48,7 +48,7 @@ class SubscribeViewModel @Inject constructor(
|
|||
private fun init() {
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
groups = articleRepository.pullGroups()
|
||||
groups = rssRepository.get().pullGroups()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class SubscribeViewModel @Inject constructor(
|
|||
articles = emptyList(),
|
||||
notificationPreset = false,
|
||||
fullContentParsePreset = false,
|
||||
selectedGroupId = null,
|
||||
selectedGroupId = "",
|
||||
groups = emptyFlow(),
|
||||
)
|
||||
}
|
||||
|
@ -73,9 +73,9 @@ class SubscribeViewModel @Inject constructor(
|
|||
private fun subscribe() {
|
||||
val feed = _viewState.value.feed ?: return
|
||||
val articles = _viewState.value.articles
|
||||
val groupId = _viewState.value.selectedGroupId ?: 0
|
||||
val groupId = _viewState.value.selectedGroupId
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
articleRepository.subscribe(
|
||||
rssRepository.get().subscribe(
|
||||
feed.copy(
|
||||
groupId = groupId,
|
||||
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 {
|
||||
it.copy(
|
||||
selectedGroupId = groupId,
|
||||
|
@ -127,7 +127,7 @@ class SubscribeViewModel @Inject constructor(
|
|||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val feedWithArticle = rssRepository.searchFeed(_viewState.value.inputContent)
|
||||
val feedWithArticle = rssHelper.searchFeed(_viewState.value.inputContent)
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
feed = feedWithArticle.feed,
|
||||
|
@ -174,7 +174,7 @@ data class SubScribeViewState(
|
|||
val articles: List<Article> = emptyList(),
|
||||
val notificationPreset: Boolean = false,
|
||||
val fullContentParsePreset: Boolean = false,
|
||||
val selectedGroupId: Int? = null,
|
||||
val selectedGroupId: String = "",
|
||||
val groups: Flow<List<Group>> = emptyFlow(),
|
||||
val pagerState: PagerState = PagerState(),
|
||||
)
|
||||
|
@ -198,7 +198,7 @@ sealed class SubscribeViewAction {
|
|||
object ChangeFullContentParsePreset : SubscribeViewAction()
|
||||
|
||||
data class SelectedGroup(
|
||||
val groupId: Int? = null
|
||||
val groupId: String
|
||||
) : SubscribeViewAction()
|
||||
|
||||
object Subscribe : SubscribeViewAction()
|
||||
|
|
|
@ -21,11 +21,11 @@ fun SubscribeViewPager(
|
|||
groups: List<Group> = emptyList(),
|
||||
selectedNotificationPreset: Boolean = false,
|
||||
selectedFullContentParsePreset: Boolean = false,
|
||||
selectedGroupId: Int = 0,
|
||||
selectedGroupId: String = "",
|
||||
pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(),
|
||||
notificationPresetOnClick: () -> Unit = {},
|
||||
fullContentParsePresetOnClick: () -> Unit = {},
|
||||
groupOnClick: (groupId: Int) -> Unit = {},
|
||||
groupOnClick: (groupId: String) -> Unit = {},
|
||||
onResultKeyboardAction: () -> Unit = {},
|
||||
) {
|
||||
ViewPager(
|
||||
|
|
|
@ -10,14 +10,14 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
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 javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ReadViewModel @Inject constructor(
|
||||
val articleRepository: ArticleRepository,
|
||||
private val rssRepository: RssRepository,
|
||||
val rssRepository: RssRepository,
|
||||
private val rssHelper: RssHelper,
|
||||
) : ViewModel() {
|
||||
private val _viewState = MutableStateFlow(ReadViewState())
|
||||
val viewState: StateFlow<ReadViewState> = _viewState.asStateFlow()
|
||||
|
@ -44,7 +44,7 @@ class ReadViewModel @Inject constructor(
|
|||
private fun renderDescriptionContent() {
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
content = rssRepository.parseDescriptionContent(
|
||||
content = rssHelper.parseDescriptionContent(
|
||||
link = it.articleWithFeed?.article?.link ?: "",
|
||||
content = it.articleWithFeed?.article?.rawDescription ?: "",
|
||||
)
|
||||
|
@ -54,7 +54,7 @@ class ReadViewModel @Inject constructor(
|
|||
|
||||
private fun renderFullContent() {
|
||||
changeLoading(true)
|
||||
rssRepository.parseFullContent(
|
||||
rssHelper.parseFullContent(
|
||||
_viewState.value.articleWithFeed?.article?.link ?: "",
|
||||
_viewState.value.articleWithFeed?.article?.title ?: ""
|
||||
) { content ->
|
||||
|
@ -76,7 +76,7 @@ class ReadViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
articleRepository.updateArticleInfo(
|
||||
rssRepository.get().updateArticleInfo(
|
||||
it.article.copy(
|
||||
isUnread = isUnread
|
||||
)
|
||||
|
@ -97,7 +97,7 @@ class ReadViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
articleRepository.updateArticleInfo(
|
||||
rssRepository.get().updateArticleInfo(
|
||||
it.article.copy(
|
||||
isStarred = isStarred
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user