From d105735453a2dbf8d6fc8f5d1c9c9cfd16bc0b5d Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 18 Mar 2022 00:15:53 +0800 Subject: [PATCH] Refactor local RSS repository to support other RSS API data sources --- app/src/main/java/me/ash/reader/App.kt | 17 +- .../main/java/me/ash/reader/DataStoreExt.kt | 5 + .../me/ash/reader/data/account/Account.kt | 5 +- .../me/ash/reader/data/article/Article.kt | 6 +- .../me/ash/reader/data/article/ArticleDao.kt | 14 +- .../ash/reader/data/article/ImportantCount.kt | 4 +- .../main/java/me/ash/reader/data/feed/Feed.kt | 11 +- .../java/me/ash/reader/data/group/Group.kt | 4 +- ...{RssNetworkModule.kt => RetrofitModule.kt} | 14 +- ...Repository.kt => AbstractRssRepository.kt} | 92 +++- .../data/repository/AccountRepository.kt | 14 +- .../data/repository/LocalRssRepository.kt | 183 ++++++++ .../reader/data/repository/OpmlRepository.kt | 4 +- .../ash/reader/data/repository/RssHelper.kt | 177 ++++++++ .../reader/data/repository/RssRepository.kt | 392 +----------------- .../reader/data/source/FeverApiDataSource.kt | 38 ++ .../me/ash/reader/data/source/FeverApiDto.kt | 54 +++ .../data/source/GoogleReaderApiDataSource.kt | 46 ++ .../reader/data/source/GoogleReaderApiDto.kt | 78 ++++ .../reader/data/source/OpmlLocalDataSource.kt | 5 +- .../me/ash/reader/ui/page/home/HomePage.kt | 2 +- .../ash/reader/ui/page/home/HomeViewModel.kt | 4 +- .../ui/page/home/article/ArticleViewModel.kt | 16 +- .../reader/ui/page/home/feed/FeedViewModel.kt | 12 +- .../home/feed/subscribe/ResultViewPage.kt | 10 +- .../home/feed/subscribe/SubscribeDialog.kt | 2 +- .../home/feed/subscribe/SubscribeViewModel.kt | 20 +- .../home/feed/subscribe/SubscribeViewPager.kt | 4 +- .../reader/ui/page/home/read/ReadViewModel.kt | 14 +- 29 files changed, 771 insertions(+), 476 deletions(-) rename app/src/main/java/me/ash/reader/data/module/{RssNetworkModule.kt => RetrofitModule.kt} (50%) rename app/src/main/java/me/ash/reader/data/repository/{ArticleRepository.kt => AbstractRssRepository.kt} (58%) create mode 100644 app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt create mode 100644 app/src/main/java/me/ash/reader/data/repository/RssHelper.kt create mode 100644 app/src/main/java/me/ash/reader/data/source/FeverApiDataSource.kt create mode 100644 app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt create mode 100644 app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDataSource.kt create mode 100644 app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDto.kt diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt index 69e4fcc..3db0055 100644 --- a/app/src/main/java/me/ash/reader/App.kt +++ b/app/src/main/java/me/ash/reader/App.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/DataStoreExt.kt b/app/src/main/java/me/ash/reader/DataStoreExt.kt index c5f0513..d02747d 100644 --- a/app/src/main/java/me/ash/reader/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/DataStoreExt.kt @@ -46,4 +46,9 @@ sealed class DataStoreKeys { override val key: Preferences.Key get() = intPreferencesKey("currentAccountId") } + + object CurrentAccountType : DataStoreKeys() { + override val key: Preferences.Key + get() = intPreferencesKey("currentAccountType") + } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/account/Account.kt b/app/src/main/java/me/ash/reader/data/account/Account.kt index 9c8a9d8..902512a 100644 --- a/app/src/main/java/me/ash/reader/data/account/Account.kt +++ b/app/src/main/java/me/ash/reader/data/account/Account.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/article/Article.kt b/app/src/main/java/me/ash/reader/data/article/Article.kt index c5cef2d..2024bcf 100644 --- a/app/src/main/java/me/ash/reader/data/article/Article.kt +++ b/app/src/main/java/me/ash/reader/data/article/Article.kt @@ -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") diff --git a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt index 481cda2..d6649f3 100644 --- a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt @@ -167,7 +167,7 @@ interface ArticleDao { ) fun queryArticleWithFeedByGroupIdWhenIsAll( accountId: Int, - groupId: Int + groupId: String, ): PagingSource @Transaction @@ -188,7 +188,7 @@ interface ArticleDao { ) fun queryArticleWithFeedByGroupIdWhenIsStarred( accountId: Int, - groupId: Int, + groupId: String, isStarred: Boolean, ): PagingSource @@ -210,7 +210,7 @@ interface ArticleDao { ) fun queryArticleWithFeedByGroupIdWhenIsUnread( accountId: Int, - groupId: Int, + groupId: String, isUnread: Boolean, ): PagingSource @@ -224,7 +224,7 @@ interface ArticleDao { ) fun queryArticleWithFeedByFeedIdWhenIsAll( accountId: Int, - feedId: Int + feedId: String ): PagingSource @Transaction @@ -238,7 +238,7 @@ interface ArticleDao { ) fun queryArticleWithFeedByFeedIdWhenIsStarred( accountId: Int, - feedId: Int, + feedId: String, isStarred: Boolean, ): PagingSource @@ -253,7 +253,7 @@ interface ArticleDao { ) fun queryArticleWithFeedByFeedIdWhenIsUnread( accountId: Int, - feedId: Int, + feedId: String, isUnread: Boolean, ): PagingSource @@ -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( diff --git a/app/src/main/java/me/ash/reader/data/article/ImportantCount.kt b/app/src/main/java/me/ash/reader/data/article/ImportantCount.kt index 8058562..7d94987 100644 --- a/app/src/main/java/me/ash/reader/data/article/ImportantCount.kt +++ b/app/src/main/java/me/ash/reader/data/article/ImportantCount.kt @@ -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, ) diff --git a/app/src/main/java/me/ash/reader/data/feed/Feed.kt b/app/src/main/java/me/ash/reader/data/feed/Feed.kt index 79663d9..f3e3a66 100644 --- a/app/src/main/java/me/ash/reader/data/feed/Feed.kt +++ b/app/src/main/java/me/ash/reader/data/feed/Feed.kt @@ -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() diff --git a/app/src/main/java/me/ash/reader/data/group/Group.kt b/app/src/main/java/me/ash/reader/data/group/Group.kt index af47e3b..cc5276a 100644 --- a/app/src/main/java/me/ash/reader/data/group/Group.kt +++ b/app/src/main/java/me/ash/reader/data/group/Group.kt @@ -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) diff --git a/app/src/main/java/me/ash/reader/data/module/RssNetworkModule.kt b/app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt similarity index 50% rename from app/src/main/java/me/ash/reader/data/module/RssNetworkModule.kt rename to app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt index 9bf9836..5a3f7ac 100644 --- a/app/src/main/java/me/ash/reader/data/module/RssNetworkModule.kt +++ b/app/src/main/java/me/ash/reader/data/module/RetrofitModule.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt similarity index 58% rename from app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt rename to app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index d97b807..bff153d 100644 --- a/app/src/main/java/me/ash/reader/data/repository/ArticleRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -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
) + + abstract suspend fun sync( + context: Context, + accountDao: AccountDao, + articleDao: ArticleDao, + feedDao: FeedDao, + rssNetworkDataSource: RssNetworkDataSource + ) + fun pullGroups(): Flow> { 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 { @@ -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
) { - 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( + 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() } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt index eee8d90..e4275d2 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt @@ -24,12 +24,12 @@ class AccountRepository @Inject constructor( return accountDao.queryAll().isEmpty() } - suspend fun addDefaultAccount(): Int { - return accountDao.insert( - Account( - name = "Feeds", - type = Account.Type.LOCAL, - ) - ).toInt() + suspend fun addDefaultAccount(): Account { + return Account( + name = "Reader You", + type = Account.Type.LOCAL, + ).apply { + id = accountDao.insert(this).toInt() + } } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt new file mode 100644 index 0000000..958be7d --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt @@ -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
) { + 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() + 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>>() + repeat(chunked.size) { + flows.add(flow { + val articles = mutableListOf
() + 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt index c3becfb..1b3e648 100644 --- a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt @@ -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) { diff --git a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt new file mode 100644 index 0000000..694b949 --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt @@ -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
() + 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
{ + val a = mutableListOf
() + 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("""() - 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( - 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() - 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>>() - repeat(chunked.size) { - flows.add(flow { - val articles = mutableListOf
() - 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
{ - val a = mutableListOf
() - 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(""" + + @Multipart + @POST("fever.php?api&feeds") + fun feeds(@Part("api_key") apiKey: RequestBody?="1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call + + 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 + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt b/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt new file mode 100644 index 0000000..8a50cde --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/source/FeverApiDto.kt @@ -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, + val feedsGroups: List, + ) + + 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, + val feedsGroups: List, + ) + + 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, + ) + + data class FaviconItem( + val id: Int, + val data: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDataSource.kt b/app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDataSource.kt new file mode 100644 index 0000000..e86932e --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDataSource.kt @@ -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 + + @Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}") + @POST("reader/api/0/subscription/list?output=json") + fun subscriptionList(): Call + + @Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}") + @POST("reader/api/0/unread-count?output=json") + fun unreadCount(): Call + + @Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}") + @POST("reader/api/0/tag/list?output=json") + fun tagList(): Call + + @Headers("Authorization:GoogleLogin auth=${"Ashinch/678592edaf9145e5b27068d9dc3afc41494ba54e"}") + @POST("reader/api/0/stream/contents/reading-list") + fun readingList(): Call + + 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 + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDto.kt b/app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDto.kt new file mode 100644 index 0000000..7823d2c --- /dev/null +++ b/app/src/main/java/me/ash/reader/data/source/GoogleReaderApiDto.kt @@ -0,0 +1,78 @@ +package me.ash.reader.data.source + +object GoogleReaderApiDto { + // subscription/list?output=json + data class SubscriptionList( + val subscriptions: List? = null, + ) + + data class SubscriptionItem( + val id: String? = null, + val title: String? = null, + val categories: List? = 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? = 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? = 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? = 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? = null, + val origin: List? = null, + val author: String? = null, + ) + + data class Summary( + val content: String? = null, + val canonical: List? = null, + val alternate: List? = null, + ) + + data class CanonicalItem( + val href: String? = null, + ) + + data class OriginItem( + val streamId: String? = null, + val title: String? = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt b/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt index 341106d..1e89b4b 100644 --- a/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt @@ -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, ), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt index ec1ec76..48524d9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt @@ -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) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 11f2056..6de72d6 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -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() } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt index b4f239b..30bc271 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/article/ArticleViewModel.kt @@ -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() diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt index 7388fbf..4fc69ca 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/FeedViewModel.kt @@ -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() - val feedImportantMap = mutableMapOf() + val groupImportantMap = mutableMapOf() + val feedImportantMap = mutableMapOf() importantList.groupBy { it.groupId }.forEach { (i, list) -> var groupImportantSum = 0 list.forEach { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt index 2628a2c..51ee7f7 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/ResultViewPage.kt @@ -28,10 +28,10 @@ fun ResultViewPage( groups: List = 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, - 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, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt index e16d390..cd9cf2c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeDialog.kt @@ -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) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt index 1b9b2d2..baf07e2 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewModel.kt @@ -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 = _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
= emptyList(), val notificationPreset: Boolean = false, val fullContentParsePreset: Boolean = false, - val selectedGroupId: Int? = null, + val selectedGroupId: String = "", val groups: Flow> = 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() diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt index 0d137f3..e5b4698 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feed/subscribe/SubscribeViewPager.kt @@ -21,11 +21,11 @@ fun SubscribeViewPager( groups: List = 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( diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt index a0a9ccf..1313b07 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt @@ -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 = _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 )