Refactor repository coroutine
This commit is contained in:
parent
004005d8be
commit
e1e43019f5
|
@ -5,10 +5,11 @@ import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
import me.ash.reader.data.repository.*
|
import me.ash.reader.data.repository.*
|
||||||
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
|
||||||
|
@ -57,9 +58,13 @@ class App : Application(), Configuration.Provider {
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
lateinit var applicationScope: CoroutineScope
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@DispatcherDefault
|
||||||
|
lateinit var dispatcherDefault: CoroutineDispatcher
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
applicationScope.launch(Dispatchers.IO) {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
accountInit()
|
accountInit()
|
||||||
workerInit()
|
workerInit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import java.io.IOException
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
val Context.currentAccountId: Int
|
val Context.currentAccountId: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
||||||
|
val Context.currentAccountType: Int
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
||||||
|
|
||||||
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
||||||
this.edit {
|
this.edit {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import androidx.paging.PagingSource
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
|
@ -32,6 +32,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
data class SyncState(
|
data class SyncState(
|
||||||
val feedCount: Int = 0,
|
val feedCount: Int = 0,
|
||||||
|
@ -59,11 +60,11 @@ abstract class AbstractRssRepository constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullGroups(): Flow<MutableList<Group>> {
|
fun pullGroups(): Flow<MutableList<Group>> {
|
||||||
return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO)
|
return groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
||||||
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(Dispatchers.IO)
|
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullArticles(
|
fun pullArticles(
|
||||||
|
@ -72,7 +73,6 @@ abstract class AbstractRssRepository constructor(
|
||||||
isStarred: Boolean = false,
|
isStarred: Boolean = false,
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): PagingSource<Int, ArticleWithFeed> {
|
): PagingSource<Int, ArticleWithFeed> {
|
||||||
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
|
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
|
@ -107,7 +107,6 @@ abstract class AbstractRssRepository constructor(
|
||||||
isStarred: Boolean = false,
|
isStarred: Boolean = false,
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): Flow<List<ImportantCount>> {
|
): Flow<List<ImportantCount>> {
|
||||||
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
|
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
|
@ -119,7 +118,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
isUnread -> articleDao
|
isUnread -> articleDao
|
||||||
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
||||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findFeedById(id: String): Feed? {
|
suspend fun findFeedById(id: String): Feed? {
|
||||||
|
@ -130,7 +129,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
return articleDao.queryById(id)
|
return articleDao.queryById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isExist(url: String): Boolean {
|
suspend fun isFeedExist(url: String): Boolean {
|
||||||
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,11 @@ import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import me.ash.reader.*
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.account.AccountDao
|
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
|
||||||
|
@ -13,11 +16,16 @@ import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
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.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
import me.ash.reader.spacerDollar
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
class FeverRssRepository @Inject constructor(
|
class FeverRssRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
|
@ -29,10 +37,17 @@ class FeverRssRepository @Inject constructor(
|
||||||
private val feverApiDataSource: FeverApiDataSource,
|
private val feverApiDataSource: FeverApiDataSource,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
rssNetworkDataSource: RssNetworkDataSource,
|
rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
@ApplicationScope
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
@DispatcherDefault
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, rssNetworkDataSource, workManager,
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
|
dispatcherIO
|
||||||
) {
|
) {
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
override suspend fun updateArticleInfo(article: Article) {
|
||||||
articleDao.update(article)
|
articleDao.update(article)
|
||||||
|
@ -58,88 +73,90 @@ class FeverRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sync() {
|
override suspend fun sync() {
|
||||||
mutex.withLock {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
val accountId = context.currentAccountId
|
mutex.withLock {
|
||||||
|
val accountId = context.currentAccountId
|
||||||
|
|
||||||
updateSyncState {
|
updateSyncState {
|
||||||
it.copy(
|
it.copy(
|
||||||
feedCount = 1,
|
feedCount = 1,
|
||||||
syncedCount = 1,
|
syncedCount = 1,
|
||||||
currentFeedName = "Fever"
|
currentFeedName = "Fever"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
|
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
|
||||||
// Temporary add feeds
|
// Temporary add feeds
|
||||||
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
|
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
|
||||||
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
|
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
|
||||||
Log.i("RLog", "Fever groups: $feverGroupsBody")
|
Log.i("RLog", "Fever groups: $feverGroupsBody")
|
||||||
feverGroupsBody.groups.forEach {
|
feverGroupsBody.groups.forEach {
|
||||||
groupDao.insert(
|
groupDao.insert(
|
||||||
Group(
|
Group(
|
||||||
|
id = accountId.spacerDollar(it.id),
|
||||||
|
name = it.title,
|
||||||
|
accountId = accountId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
|
||||||
|
feverGroupsBody.feeds_groups.forEach { item ->
|
||||||
|
item.feed_ids
|
||||||
|
.split(",")
|
||||||
|
.map { it.toInt() }
|
||||||
|
.forEach { id ->
|
||||||
|
feverFeedsGroupsMap[id] = item.group_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val feeds = feverFeeds.map {
|
||||||
|
Feed(
|
||||||
id = accountId.spacerDollar(it.id),
|
id = accountId.spacerDollar(it.id),
|
||||||
name = it.title,
|
name = it.title,
|
||||||
accountId = accountId,
|
url = it.url,
|
||||||
|
groupId = feverFeedsGroupsMap[it.id].toString(),
|
||||||
|
accountId = accountId
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
feedDao.insertList(feeds)
|
||||||
}
|
}
|
||||||
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
|
|
||||||
feverGroupsBody.feeds_groups.forEach { item ->
|
|
||||||
item.feed_ids
|
|
||||||
.split(",")
|
|
||||||
.map { it.toInt() }
|
|
||||||
.forEach { id ->
|
|
||||||
feverFeedsGroupsMap[id] = item.group_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val feeds = feverFeeds.map {
|
|
||||||
Feed(
|
|
||||||
id = accountId.spacerDollar(it.id),
|
|
||||||
name = it.title,
|
|
||||||
url = it.url,
|
|
||||||
groupId = feverFeedsGroupsMap[it.id].toString(),
|
|
||||||
accountId = accountId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
feedDao.insertList(feeds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add articles
|
// Add articles
|
||||||
val articles = mutableListOf<Article>()
|
val articles = mutableListOf<Article>()
|
||||||
feverApiDataSource.itemsBySince(since = 1647444325925621L)
|
feverApiDataSource.itemsBySince(since = 1647444325925621L)
|
||||||
.execute().body()!!.items
|
.execute().body()!!.items
|
||||||
.forEach {
|
.forEach {
|
||||||
articles.add(
|
articles.add(
|
||||||
Article(
|
Article(
|
||||||
id = accountId.spacerDollar(it.id),
|
id = accountId.spacerDollar(it.id),
|
||||||
date = Date(it.created_on_time * 1000),
|
date = Date(it.created_on_time * 1000),
|
||||||
title = it.title,
|
title = it.title,
|
||||||
author = it.author,
|
author = it.author,
|
||||||
rawDescription = it.html,
|
rawDescription = it.html,
|
||||||
shortDescription = (
|
shortDescription = (
|
||||||
Readability4JExtended("", it.html)
|
Readability4JExtended("", it.html)
|
||||||
.parse().textContent ?: ""
|
.parse().textContent ?: ""
|
||||||
).take(100).trim(),
|
).take(100).trim(),
|
||||||
link = it.url,
|
link = it.url,
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
feedId = it.feed_id.toString(),
|
feedId = it.feed_id.toString(),
|
||||||
isUnread = it.is_read == 0,
|
isUnread = it.is_read == 0,
|
||||||
isStarred = it.is_saved == 1,
|
isStarred = it.is_saved == 1,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
articleDao.insertList(articles)
|
||||||
|
|
||||||
|
// Complete sync
|
||||||
|
accountDao.update(accountDao.queryById(accountId)!!.apply {
|
||||||
|
updateAt = Date()
|
||||||
|
})
|
||||||
|
updateSyncState {
|
||||||
|
it.copy(
|
||||||
|
feedCount = 0,
|
||||||
|
syncedCount = 0,
|
||||||
|
currentFeedName = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
articleDao.insertList(articles)
|
|
||||||
|
|
||||||
// Complete sync
|
|
||||||
accountDao.update(accountDao.queryById(accountId)!!.apply {
|
|
||||||
updateAt = Date()
|
|
||||||
})
|
|
||||||
updateSyncState {
|
|
||||||
it.copy(
|
|
||||||
feedCount = 0,
|
|
||||||
syncedCount = 0,
|
|
||||||
currentFeedName = ""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,7 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import me.ash.reader.MainActivity
|
import me.ash.reader.MainActivity
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
|
@ -26,6 +22,9 @@ import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
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.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.ui.page.common.ExtraName
|
import me.ash.reader.ui.page.common.ExtraName
|
||||||
import me.ash.reader.ui.page.common.NotificationGroupName
|
import me.ash.reader.ui.page.common.NotificationGroupName
|
||||||
|
@ -41,10 +40,17 @@ class LocalRssRepository @Inject constructor(
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
|
@ApplicationScope
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
@DispatcherDefault
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, rssNetworkDataSource, workManager,
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
|
dispatcherIO
|
||||||
) {
|
) {
|
||||||
private val notificationManager: NotificationManager =
|
private val notificationManager: NotificationManager =
|
||||||
(getSystemService(
|
(getSystemService(
|
||||||
|
@ -84,40 +90,38 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sync() {
|
override suspend fun sync() {
|
||||||
mutex.withLock {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
withContext(Dispatchers.IO) {
|
val preTime = System.currentTimeMillis()
|
||||||
val preTime = System.currentTimeMillis()
|
val accountId = context.currentAccountId
|
||||||
val accountId = context.currentAccountId
|
val articles = mutableListOf<Article>()
|
||||||
val articles = mutableListOf<Article>()
|
feedDao.queryAll(accountId)
|
||||||
feedDao.queryAll(accountId)
|
.also { feed -> updateSyncState { it.copy(feedCount = feed.size) } }
|
||||||
.also { feed -> updateSyncState { it.copy(feedCount = feed.size) } }
|
.map { feed -> async { syncFeed(feed) } }
|
||||||
.map { feed -> async { syncFeed(feed) } }
|
.awaitAll()
|
||||||
.awaitAll()
|
.forEach {
|
||||||
.forEach {
|
if (it.isNotify) {
|
||||||
if (it.isNotify) {
|
notify(it.articles)
|
||||||
notify(it.articles)
|
|
||||||
}
|
|
||||||
articles.addAll(it.articles)
|
|
||||||
}
|
}
|
||||||
|
articles.addAll(it.articles)
|
||||||
|
}
|
||||||
|
|
||||||
articleDao.insertList(articles)
|
articleDao.insertList(articles)
|
||||||
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||||
accountDao.queryById(accountId)?.let { account ->
|
accountDao.queryById(accountId)?.let { account ->
|
||||||
accountDao.update(
|
accountDao.update(
|
||||||
account.apply {
|
account.apply {
|
||||||
updateAt = Date()
|
updateAt = Date()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
updateSyncState {
|
|
||||||
it.copy(
|
|
||||||
feedCount = 0,
|
|
||||||
syncedCount = 0,
|
|
||||||
currentFeedName = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
updateSyncState {
|
||||||
|
it.copy(
|
||||||
|
feedCount = 0,
|
||||||
|
syncedCount = 0,
|
||||||
|
currentFeedName = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ArticleNotify(
|
data class ArticleNotify(
|
||||||
|
@ -127,10 +131,20 @@ class LocalRssRepository @Inject constructor(
|
||||||
|
|
||||||
private suspend fun syncFeed(feed: Feed): ArticleNotify {
|
private suspend fun syncFeed(feed: Feed): ArticleNotify {
|
||||||
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
||||||
val articles = rssHelper.queryRssXml(feed, latest?.link).also {
|
var articles: List<Article>? = null
|
||||||
if (feed.icon == null && it.isNotEmpty()) {
|
try {
|
||||||
rssHelper.queryRssIcon(feedDao, feed, it.first().link)
|
articles = rssHelper.queryRssXml(feed, latest?.link)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
|
||||||
|
return ArticleNotify(listOf(), false)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (feed.icon == null && !articles.isNullOrEmpty()) {
|
||||||
|
rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
||||||
|
return ArticleNotify(listOf(), false)
|
||||||
}
|
}
|
||||||
updateSyncState {
|
updateSyncState {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
package me.ash.reader.data.repository
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import be.ceau.opml.OpmlWriter
|
import be.ceau.opml.OpmlWriter
|
||||||
import be.ceau.opml.entity.Body
|
import be.ceau.opml.entity.Body
|
||||||
import be.ceau.opml.entity.Head
|
import be.ceau.opml.entity.Head
|
||||||
import be.ceau.opml.entity.Opml
|
import be.ceau.opml.entity.Opml
|
||||||
import be.ceau.opml.entity.Outline
|
import be.ceau.opml.entity.Outline
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import me.ash.reader.R
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.account.AccountDao
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||||
|
import me.ash.reader.spacerDollar
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -25,69 +26,70 @@ class OpmlRepository @Inject constructor(
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlLocalDataSource: OpmlLocalDataSource
|
private val opmlLocalDataSource: OpmlLocalDataSource,
|
||||||
|
private val stringsRepository: StringsRepository,
|
||||||
) {
|
) {
|
||||||
|
@Throws(Exception::class)
|
||||||
suspend fun saveToDatabase(inputStream: InputStream) {
|
suspend fun saveToDatabase(inputStream: InputStream) {
|
||||||
try {
|
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
|
||||||
val defaultGroup = groupDao.queryById(opmlLocalDataSource.getDefaultGroupId())!!
|
val groupWithFeedList =
|
||||||
val groupWithFeedList =
|
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
|
||||||
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
|
groupWithFeedList.forEach { groupWithFeed ->
|
||||||
groupWithFeedList.forEach { groupWithFeed ->
|
if (groupWithFeed.group != defaultGroup) {
|
||||||
if (groupWithFeed.group != defaultGroup) {
|
groupDao.insert(groupWithFeed.group)
|
||||||
groupDao.insert(groupWithFeed.group)
|
|
||||||
}
|
|
||||||
val repeatList = mutableListOf<Feed>()
|
|
||||||
groupWithFeed.feeds.forEach {
|
|
||||||
it.groupId = groupWithFeed.group.id
|
|
||||||
if (rssRepository.get().isExist(it.url)) {
|
|
||||||
repeatList.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
val repeatList = mutableListOf<Feed>()
|
||||||
Log.e("saveToDatabase", "${e.message}")
|
groupWithFeed.feeds.forEach {
|
||||||
|
it.groupId = groupWithFeed.group.id
|
||||||
|
if (rssRepository.get().isFeedExist(it.url)) {
|
||||||
|
repeatList.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveToString(): String? =
|
@Throws(Exception::class)
|
||||||
try {
|
suspend fun saveToString(): String {
|
||||||
val defaultGroup = groupDao.queryById(opmlLocalDataSource.getDefaultGroupId())!!
|
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
|
||||||
OpmlWriter().write(
|
return OpmlWriter().write(
|
||||||
Opml(
|
Opml(
|
||||||
"2.0",
|
"2.0",
|
||||||
Head(
|
Head(
|
||||||
accountDao.queryById(context.currentAccountId).name,
|
accountDao.queryById(context.currentAccountId).name,
|
||||||
Date().toString(), null, null, null,
|
Date().toString(), null, null, null,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
),
|
),
|
||||||
Body(groupDao.queryAllGroupWithFeed(context.currentAccountId).map {
|
Body(groupDao.queryAllGroupWithFeed(context.currentAccountId).map {
|
||||||
Outline(
|
Outline(
|
||||||
mapOf(
|
mapOf(
|
||||||
"text" to it.group.name,
|
"text" to it.group.name,
|
||||||
"title" to it.group.name,
|
"title" to it.group.name,
|
||||||
"isDefault" to (it.group.id == defaultGroup.id).toString()
|
"isDefault" to (it.group.id == defaultGroup.id).toString()
|
||||||
),
|
),
|
||||||
it.feeds.map { feed ->
|
it.feeds.map { feed ->
|
||||||
Outline(
|
Outline(
|
||||||
mapOf(
|
mapOf(
|
||||||
"text" to feed.name,
|
"text" to feed.name,
|
||||||
"title" to feed.name,
|
"title" to feed.name,
|
||||||
"xmlUrl" to feed.url,
|
"xmlUrl" to feed.url,
|
||||||
"htmlUrl" to feed.url,
|
"htmlUrl" to feed.url,
|
||||||
"isNotification" to feed.isNotification.toString(),
|
"isNotification" to feed.isNotification.toString(),
|
||||||
"isFullContent" to feed.isFullContent.toString(),
|
"isFullContent" to feed.isFullContent.toString(),
|
||||||
),
|
),
|
||||||
listOf()
|
listOf()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
)!!
|
||||||
Log.e("saveToString", "${e.message}")
|
}
|
||||||
null
|
|
||||||
}
|
private fun getDefaultGroupId(): String {
|
||||||
|
val readYouString = stringsRepository.getString(R.string.read_you)
|
||||||
|
val defaultString = stringsRepository.getString(R.string.defaults)
|
||||||
|
return context.currentAccountId.spacerDollar(readYouString + defaultString)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,17 +4,20 @@ import android.content.Context
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.feed.FeedWithArticle
|
import me.ash.reader.data.feed.FeedWithArticle
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.spacerDollar
|
import me.ash.reader.spacerDollar
|
||||||
import net.dankito.readability4j.Readability4J
|
import net.dankito.readability4j.Readability4J
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import okhttp3.*
|
import okhttp3.OkHttpClient
|
||||||
import java.io.IOException
|
import okhttp3.Request
|
||||||
import java.text.ParsePosition
|
import java.text.ParsePosition
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -24,19 +27,23 @@ class RssHelper @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||||
val accountId = context.currentAccountId
|
return withContext(dispatcherIO) {
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
val accountId = context.currentAccountId
|
||||||
val feed = Feed(
|
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
||||||
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
val feed = Feed(
|
||||||
name = parseRss.title!!,
|
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
||||||
url = feedLink,
|
name = parseRss.title!!,
|
||||||
groupId = "",
|
url = feedLink,
|
||||||
accountId = accountId,
|
groupId = "",
|
||||||
)
|
accountId = accountId,
|
||||||
return FeedWithArticle(feed, queryRssXml(feed))
|
)
|
||||||
|
FeedWithArticle(feed, queryRssXml(feed))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseDescriptionContent(link: String, content: String): String {
|
fun parseDescriptionContent(link: String, content: String): String {
|
||||||
|
@ -46,42 +53,39 @@ class RssHelper @Inject constructor(
|
||||||
return element.toString()
|
return element.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseFullContent(link: String, title: String, callback: (String) -> Unit) {
|
@Throws(Exception::class)
|
||||||
OkHttpClient()
|
suspend fun parseFullContent(link: String, title: String): String {
|
||||||
.newCall(Request.Builder().url(link).build())
|
return withContext(dispatcherIO) {
|
||||||
.enqueue(object : Callback {
|
val response = OkHttpClient()
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
.newCall(Request.Builder().url(link).build())
|
||||||
callback(e.message.toString())
|
.execute()
|
||||||
|
val content = response.body!!.string()
|
||||||
|
val readability4J: Readability4J =
|
||||||
|
Readability4JExtended(link, content)
|
||||||
|
val articleContent = readability4J.parse().articleContent
|
||||||
|
if (articleContent == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
val h1Element = articleContent.selectFirst("h1")
|
||||||
|
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
|
||||||
|
h1Element.remove()
|
||||||
}
|
}
|
||||||
|
articleContent.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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
suspend fun queryRssXml(
|
suspend fun queryRssXml(
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
latestLink: String? = null,
|
latestLink: String? = null,
|
||||||
): List<Article> {
|
): List<Article> {
|
||||||
val a = mutableListOf<Article>()
|
return withContext(dispatcherIO) {
|
||||||
try {
|
val a = mutableListOf<Article>()
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
||||||
parseRss.items.forEach {
|
parseRss.items.forEach {
|
||||||
if (latestLink != null && latestLink == it.link) return a
|
if (latestLink != null && latestLink == it.link) return@withContext a
|
||||||
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
|
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
|
||||||
a.add(
|
a.add(
|
||||||
Article(
|
Article(
|
||||||
|
@ -104,63 +108,57 @@ class RssHelper @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return a
|
a
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("RLog", "error ${feed.name}: ${e.message}")
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
suspend fun queryRssIcon(
|
suspend fun queryRssIcon(
|
||||||
feedDao: FeedDao,
|
feedDao: FeedDao,
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
articleLink: String?,
|
articleLink: String,
|
||||||
) {
|
) {
|
||||||
try {
|
withContext(dispatcherIO) {
|
||||||
if (articleLink == null) return
|
|
||||||
val execute = OkHttpClient()
|
val execute = OkHttpClient()
|
||||||
.newCall(Request.Builder().url(articleLink).build())
|
.newCall(Request.Builder().url(articleLink).build())
|
||||||
.execute()
|
.execute()
|
||||||
val content = execute.body?.string()
|
val content = execute.body!!.string()
|
||||||
val regex =
|
val regex =
|
||||||
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
|
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
|
||||||
if (content != null) {
|
var iconLink = regex
|
||||||
var iconLink = regex
|
.find(content)
|
||||||
.find(content)
|
?.groups?.get(3)
|
||||||
?.groups?.get(3)
|
?.value
|
||||||
?.value
|
Log.i("rlog", "queryRssIcon: $iconLink")
|
||||||
Log.i("rlog", "queryRssIcon: $iconLink")
|
if (iconLink != null) {
|
||||||
if (iconLink != null) {
|
if (iconLink.startsWith("//")) {
|
||||||
if (iconLink.startsWith("//")) {
|
iconLink = "http:$iconLink"
|
||||||
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, "")
|
|
||||||
}
|
}
|
||||||
|
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 {
|
} else {
|
||||||
// saveRssIcon(feedDao, feed, "")
|
// saveRssIcon(feedDao, feed, "")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("RLog", "queryRssIcon: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
|
@Throws(Exception::class)
|
||||||
val execute = OkHttpClient()
|
suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
|
||||||
.newCall(Request.Builder().url(iconLink).build())
|
withContext(dispatcherIO) {
|
||||||
.execute()
|
val response = OkHttpClient()
|
||||||
feedDao.update(
|
.newCall(Request.Builder().url(iconLink).build())
|
||||||
feed.apply {
|
.execute()
|
||||||
icon = execute.body?.bytes()
|
feedDao.update(
|
||||||
}
|
feed.apply {
|
||||||
)
|
icon = response.body!!.bytes()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDate(
|
private fun parseDate(
|
||||||
|
|
|
@ -2,10 +2,8 @@ package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.currentAccountType
|
||||||
import me.ash.reader.data.account.Account
|
import me.ash.reader.data.account.Account
|
||||||
import me.ash.reader.dataStore
|
|
||||||
import me.ash.reader.get
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RssRepository @Inject constructor(
|
class RssRepository @Inject constructor(
|
||||||
|
@ -15,13 +13,11 @@ class RssRepository @Inject constructor(
|
||||||
private val feverRssRepository: FeverRssRepository,
|
private val feverRssRepository: FeverRssRepository,
|
||||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
) {
|
) {
|
||||||
fun get() = when (getAccountType()) {
|
fun get() = when (context.currentAccountType) {
|
||||||
Account.Type.LOCAL -> localRssRepository
|
Account.Type.LOCAL -> localRssRepository
|
||||||
// Account.Type.LOCAL -> feverRssRepository
|
// Account.Type.LOCAL -> feverRssRepository
|
||||||
Account.Type.FEVER -> feverRssRepository
|
Account.Type.FEVER -> feverRssRepository
|
||||||
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
||||||
else -> throw IllegalStateException("Unknown account type: ${getAccountType()}")
|
else -> throw IllegalStateException("Unknown account type: ${context.currentAccountType}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAccountType(): Int = context.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ package me.ash.reader.data.source
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import be.ceau.opml.OpmlParser
|
import be.ceau.opml.OpmlParser
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.ash.reader.currentAccountId
|
||||||
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.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
import me.ash.reader.data.repository.StringsRepository
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -15,79 +17,77 @@ import javax.inject.Inject
|
||||||
class OpmlLocalDataSource @Inject constructor(
|
class OpmlLocalDataSource @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val stringsRepository: StringsRepository,
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
fun getDefaultGroupId(): String {
|
@Throws(Exception::class)
|
||||||
val readYouString = stringsRepository.getString(R.string.read_you)
|
suspend fun parseFileInputStream(
|
||||||
val defaultString = stringsRepository.getString(R.string.defaults)
|
inputStream: InputStream,
|
||||||
return context.dataStore
|
defaultGroup: Group
|
||||||
.get(DataStoreKeys.CurrentAccountId)!!
|
): List<GroupWithFeed> {
|
||||||
.spacerDollar(readYouString + defaultString)
|
return withContext(dispatcherIO) {
|
||||||
}
|
val accountId = context.currentAccountId
|
||||||
|
val opml = OpmlParser().parse(inputStream)
|
||||||
|
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
|
||||||
|
it.addGroup(defaultGroup)
|
||||||
|
}
|
||||||
|
|
||||||
// @Throws(XmlPullParserException::class, IOException::class)
|
opml.body.outlines.forEach {
|
||||||
fun parseFileInputStream(inputStream: InputStream, defaultGroup: Group): List<GroupWithFeed> {
|
// Only feeds
|
||||||
val accountId = context.currentAccountId
|
if (it.subElements.isEmpty()) {
|
||||||
val opml = OpmlParser().parse(inputStream)
|
// It's a empty group
|
||||||
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
|
if (it.attributes["xmlUrl"] == null) {
|
||||||
it.addGroup(defaultGroup)
|
if (!it.attributes["isDefault"].toBoolean()) {
|
||||||
}
|
groupWithFeedList.addGroup(
|
||||||
|
Group(
|
||||||
opml.body.outlines.forEach {
|
id = UUID.randomUUID().toString(),
|
||||||
// Only feeds
|
name = it.attributes["title"] ?: it.text!!,
|
||||||
if (it.subElements.isEmpty()) {
|
accountId = accountId,
|
||||||
// It's a empty group
|
)
|
||||||
if (it.attributes["xmlUrl"] == null) {
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
groupWithFeedList.addFeedToDefault(
|
||||||
|
Feed(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
name = it.attributes["title"] ?: it.text!!,
|
||||||
|
url = it.attributes["xmlUrl"]!!,
|
||||||
|
groupId = defaultGroup.id,
|
||||||
|
accountId = accountId,
|
||||||
|
isNotification = it.attributes["isNotification"].toBoolean(),
|
||||||
|
isFullContent = it.attributes["isFullContent"].toBoolean(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var groupId = defaultGroup.id
|
||||||
if (!it.attributes["isDefault"].toBoolean()) {
|
if (!it.attributes["isDefault"].toBoolean()) {
|
||||||
|
groupId = UUID.randomUUID().toString()
|
||||||
groupWithFeedList.addGroup(
|
groupWithFeedList.addGroup(
|
||||||
Group(
|
Group(
|
||||||
id = UUID.randomUUID().toString(),
|
id = groupId,
|
||||||
name = it.attributes["title"] ?: it.text!!,
|
name = it.attributes["title"] ?: it.text!!,
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
it.subElements.forEach { outline ->
|
||||||
groupWithFeedList.addFeedToDefault(
|
groupWithFeedList.addFeed(
|
||||||
Feed(
|
Feed(
|
||||||
id = UUID.randomUUID().toString(),
|
id = UUID.randomUUID().toString(),
|
||||||
name = it.attributes["title"] ?: it.text!!,
|
name = outline.attributes["title"] ?: outline.text!!,
|
||||||
url = it.attributes["xmlUrl"]!!,
|
url = outline.attributes["xmlUrl"]!!,
|
||||||
groupId = defaultGroup.id,
|
groupId = groupId,
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
isNotification = it.attributes["isNotification"].toBoolean(),
|
isNotification = outline.attributes["isNotification"].toBoolean(),
|
||||||
isFullContent = it.attributes["isFullContent"].toBoolean(),
|
isFullContent = outline.attributes["isFullContent"].toBoolean(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var groupId = defaultGroup.id
|
|
||||||
if (!it.attributes["isDefault"].toBoolean()) {
|
|
||||||
groupId = UUID.randomUUID().toString()
|
|
||||||
groupWithFeedList.addGroup(
|
|
||||||
Group(
|
|
||||||
id = groupId,
|
|
||||||
name = it.attributes["title"] ?: it.text!!,
|
|
||||||
accountId = accountId,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
it.subElements.forEach { outline ->
|
|
||||||
groupWithFeedList.addFeed(
|
|
||||||
Feed(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
name = outline.attributes["title"] ?: outline.text!!,
|
|
||||||
url = outline.attributes["xmlUrl"]!!,
|
|
||||||
groupId = groupId,
|
|
||||||
accountId = accountId,
|
|
||||||
isNotification = outline.attributes["isNotification"].toBoolean(),
|
|
||||||
isFullContent = outline.attributes["isFullContent"].toBoolean(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
groupWithFeedList
|
||||||
}
|
}
|
||||||
return groupWithFeedList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<GroupWithFeed>.addGroup(group: Group) {
|
private fun MutableList<GroupWithFeed>.addGroup(group: Group) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.drawer.feed
|
package me.ash.reader.ui.page.home.drawer.feed
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
@ -8,10 +9,10 @@ import androidx.compose.material.icons.rounded.DeleteOutline
|
||||||
import androidx.compose.material.icons.rounded.RssFeed
|
import androidx.compose.material.icons.rounded.RssFeed
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
@ -31,7 +32,6 @@ fun FeedOptionDrawer(
|
||||||
viewModel: FeedOptionViewModel = hiltViewModel(),
|
viewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
content: @Composable () -> Unit = {},
|
content: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
val feed = viewState.feed
|
val feed = viewState.feed
|
||||||
|
|
||||||
|
|
|
@ -49,14 +49,22 @@ class FeedsViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun importFromInputStream(inputStream: InputStream) {
|
private fun importFromInputStream(inputStream: InputStream) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
opmlRepository.saveToDatabase(inputStream)
|
try {
|
||||||
rssRepository.get().doSync()
|
opmlRepository.saveToDatabase(inputStream)
|
||||||
|
rssRepository.get().doSync()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FeedsViewModel", "importFromInputStream: ", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportAsOpml(callback: (String) -> Unit = {}) {
|
private fun exportAsOpml(callback: (String) -> Unit = {}) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
opmlRepository.saveToString()?.let { callback(it) }
|
try {
|
||||||
|
callback(opmlRepository.saveToString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FeedsViewModel", "exportAsOpml: ", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +82,6 @@ class FeedsViewModel @Inject constructor(
|
||||||
rssRepository.get().pullFeeds(),
|
rssRepository.get().pullFeeds(),
|
||||||
rssRepository.get().pullImportant(isStarred, isUnread),
|
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||||
) { groupWithFeedList, importantList ->
|
) { groupWithFeedList, importantList ->
|
||||||
Log.i("RLog", "thread:combine ${Thread.currentThread().name}")
|
|
||||||
val groupImportantMap = mutableMapOf<String, Int>()
|
val groupImportantMap = mutableMapOf<String, Int>()
|
||||||
val feedImportantMap = mutableMapOf<String, Int>()
|
val feedImportantMap = mutableMapOf<String, Int>()
|
||||||
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
||||||
|
@ -109,8 +116,6 @@ class FeedsViewModel @Inject constructor(
|
||||||
}.onStart {
|
}.onStart {
|
||||||
|
|
||||||
}.onEach { groupWithFeedList ->
|
}.onEach { groupWithFeedList ->
|
||||||
Log.i("RLog", "thread:onEach ${Thread.currentThread().name}")
|
|
||||||
|
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
filter = when {
|
filter = when {
|
||||||
|
|
|
@ -148,7 +148,7 @@ class SubscribeViewModel @Inject constructor(
|
||||||
lockLinkInput = true,
|
lockLinkInput = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (rssRepository.get().isExist(_viewState.value.linkContent)) {
|
if (rssRepository.get().isFeedExist(_viewState.value.linkContent)) {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
title = stringsRepository.getString(R.string.subscribe),
|
title = stringsRepository.getString(R.string.subscribe),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -55,7 +54,6 @@ class FlowViewModel @Inject constructor(
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
||||||
Log.i("RLog", "thread:Pager ${Thread.currentThread().name}")
|
|
||||||
rssRepository.get().pullArticles(
|
rssRepository.get().pullArticles(
|
||||||
groupId = filterState.group?.id,
|
groupId = filterState.group?.id,
|
||||||
feedId = filterState.feed?.id,
|
feedId = filterState.feed?.id,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.read
|
package me.ash.reader.ui.page.home.read
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -55,12 +56,23 @@ class ReadViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun renderFullContent() {
|
private fun renderFullContent() {
|
||||||
changeLoading(true)
|
changeLoading(true)
|
||||||
rssHelper.parseFullContent(
|
viewModelScope.launch {
|
||||||
_viewState.value.articleWithFeed?.article?.link ?: "",
|
try {
|
||||||
_viewState.value.articleWithFeed?.article?.title ?: ""
|
_viewState.update {
|
||||||
) { content ->
|
it.copy(
|
||||||
_viewState.update {
|
content = rssHelper.parseFullContent(
|
||||||
it.copy(content = content)
|
_viewState.value.articleWithFeed?.article?.link ?: "",
|
||||||
|
_viewState.value.articleWithFeed?.article?.title ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i("RLog", "renderFullContent: ${e.message}")
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
content = e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user