diff --git a/app/src/main/java/me/ash/reader/domain/data/ArticlePagingListUseCase.kt b/app/src/main/java/me/ash/reader/domain/data/ArticlePagingListUseCase.kt index cf8b950eb..439186f0a 100644 --- a/app/src/main/java/me/ash/reader/domain/data/ArticlePagingListUseCase.kt +++ b/app/src/main/java/me/ash/reader/domain/data/ArticlePagingListUseCase.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -72,8 +73,17 @@ constructor( .combine(accountService.currentAccountIdFlow) { filterState, accountId -> filterState } - .collect { filterState -> + .combine(settingsProvider.settingsFlow) { filterState, settings -> + filterState to settings + } + .distinctUntilChanged { (oldState, oldSettings), (newState, newSettings) -> + oldState == newState && + oldSettings.hideDuplicateArticles == newSettings.hideDuplicateArticles && + oldSettings.flowSortUnreadArticles == newSettings.flowSortUnreadArticles + } + .collect { (filterState, settings) -> val searchContent = filterState.searchContent + val hideDuplicates = settings.hideDuplicateArticles.value mutablePagerFlow.value = PagerData( @@ -90,8 +100,9 @@ constructor( isStarred = filterState.filter.isStarred(), isUnread = filterState.filter.isUnread(), sortAscending = - settingsProvider.settings.flowSortUnreadArticles + settings.flowSortUnreadArticles .value, + hideDuplicates = hideDuplicates, ) } else { rssService @@ -102,8 +113,9 @@ constructor( isStarred = filterState.filter.isStarred(), isUnread = filterState.filter.isUnread(), sortAscending = - settingsProvider.settings.flowSortUnreadArticles + settings.flowSortUnreadArticles .value, + hideDuplicates = hideDuplicates, ) } } diff --git a/app/src/main/java/me/ash/reader/domain/data/GroupWithFeedsListUseCase.kt b/app/src/main/java/me/ash/reader/domain/data/GroupWithFeedsListUseCase.kt index 1eb98f3cf..bc824bc97 100644 --- a/app/src/main/java/me/ash/reader/domain/data/GroupWithFeedsListUseCase.kt +++ b/app/src/main/java/me/ash/reader/domain/data/GroupWithFeedsListUseCase.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -50,12 +51,18 @@ class GroupWithFeedsListUseCase @Inject constructor( filterStateUseCase.filterStateFlow.map { it.filter } .combine(accountFlow) { filter, account -> filter + }.combine( + settingsProvider.settingsFlow + .map { it.hideDuplicateArticles.value } + .distinctUntilChanged() + ) { filter, hideDuplicates -> + filter to hideDuplicates }.collectLatest { currentJob?.cancel() - currentJob = when (it) { - Filter.Unread -> pullUnreadFeeds() + currentJob = when (it.first) { + Filter.Unread -> pullUnreadFeeds(it.second) Filter.Starred -> pullStarredFeeds() - else -> pullAllFeeds() + else -> pullAllFeeds(it.second) } } } @@ -72,9 +79,9 @@ class GroupWithFeedsListUseCase @Inject constructor( private val hideEmptyGroups get() = settingsProvider.settings.hideEmptyGroups.value @OptIn(ExperimentalCoroutinesApi::class) - private fun pullAllFeeds(): Job { + private fun pullAllFeeds(hideDuplicates: Boolean): Job { val articleCountMapFlow = - rssService.get().pullImportant(isStarred = false, isUnread = false) + rssService.get().pullImportant(isStarred = false, isUnread = false, hideDuplicates = hideDuplicates) return applicationScope.launch { feedsFlow.combine(articleCountMapFlow) { groupWithFeedsList, articleCountMap -> @@ -128,8 +135,9 @@ class GroupWithFeedsListUseCase @Inject constructor( } @OptIn(FlowPreview::class) - private fun pullUnreadFeeds(): Job { - val unreadCountMapFlow = rssService.get().pullImportant(isStarred = false, isUnread = true) + private fun pullUnreadFeeds(hideDuplicates: Boolean): Job { + val unreadCountMapFlow = + rssService.get().pullImportant(isStarred = false, isUnread = true, hideDuplicates = hideDuplicates) return applicationScope.launch { combine( feedsFlow, unreadCountMapFlow, diffMapHolder.diffMapSnapshotFlow diff --git a/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt b/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt index e50f9603f..e1f2a59a9 100644 --- a/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt @@ -87,20 +87,31 @@ interface ArticleDao { @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId - AND feedId IN ( + SELECT * FROM article AS a + WHERE a.accountId = :accountId + AND a.feedId IN ( SELECT id FROM feed WHERE groupId = :groupId ) - AND isUnread = :isUnread + AND a.isUnread = :isUnread AND ( - title LIKE '%' || :text || '%' - OR shortDescription LIKE '%' || :text || '%' - OR fullContent LIKE '%' || :text || '%' + a.title LIKE '%' || :text || '%' + OR a.shortDescription LIKE '%' || :text || '%' + OR a.fullContent LIKE '%' || :text || '%' + ) + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun searchArticleByGroupIdWhenIsUnread( @@ -108,7 +119,8 @@ interface ArticleDao { text: String, groupId: String, isUnread: Boolean, - sortAscending: Boolean = false + sortAscending: Boolean = false, + hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -141,40 +153,62 @@ interface ArticleDao { @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId - AND feedId IN ( + SELECT * FROM article AS a + WHERE a.accountId = :accountId + AND a.feedId IN ( SELECT id FROM feed WHERE groupId = :groupId ) AND ( - title LIKE '%' || :text || '%' - OR shortDescription LIKE '%' || :text || '%' - OR fullContent LIKE '%' || :text || '%' + a.title LIKE '%' || :text || '%' + OR a.shortDescription LIKE '%' || :text || '%' + OR a.fullContent LIKE '%' || :text || '%' + ) + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun searchArticleByGroupIdWhenAll( - accountId: Int, text: String, groupId: String, sortAscending: Boolean = false + accountId: Int, text: String, groupId: String, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId - AND feedId = :feedId - AND isUnread = :isUnread + SELECT * FROM article AS a + WHERE a.accountId = :accountId + AND a.feedId = :feedId + AND a.isUnread = :isUnread AND ( - title LIKE '%' || :text || '%' - OR shortDescription LIKE '%' || :text || '%' - OR fullContent LIKE '%' || :text || '%' + a.title LIKE '%' || :text || '%' + OR a.shortDescription LIKE '%' || :text || '%' + OR a.fullContent LIKE '%' || :text || '%' + ) + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun searchArticleByFeedIdWhenIsUnread( @@ -182,7 +216,8 @@ interface ArticleDao { text: String, feedId: String, isUnread: Boolean, - sortAscending: Boolean = false + sortAscending: Boolean = false, + hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -213,41 +248,63 @@ interface ArticleDao { @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId - AND feedId = :feedId + SELECT * FROM article AS a + WHERE a.accountId = :accountId + AND a.feedId = :feedId AND ( - title LIKE '%' || :text || '%' - OR shortDescription LIKE '%' || :text || '%' - OR fullContent LIKE '%' || :text || '%' + a.title LIKE '%' || :text || '%' + OR a.shortDescription LIKE '%' || :text || '%' + OR a.fullContent LIKE '%' || :text || '%' + ) + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun searchArticleByFeedIdWhenAll( - accountId: Int, text: String, feedId: String, sortAscending: Boolean = false + accountId: Int, text: String, feedId: String, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId - AND isUnread = :isUnread + SELECT * FROM article AS a + WHERE a.accountId = :accountId + AND a.isUnread = :isUnread AND ( - title LIKE '%' || :text || '%' - OR shortDescription LIKE '%' || :text || '%' - OR fullContent LIKE '%' || :text || '%' + a.title LIKE '%' || :text || '%' + OR a.shortDescription LIKE '%' || :text || '%' + OR a.fullContent LIKE '%' || :text || '%' + ) + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun searchArticleWhenIsUnread( - accountId: Int, text: String, isUnread: Boolean, sortAscending: Boolean = false + accountId: Int, text: String, isUnread: Boolean, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -273,20 +330,31 @@ interface ArticleDao { @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId + SELECT * FROM article AS a + WHERE a.accountId = :accountId AND ( - title LIKE '%' || :text || '%' - OR shortDescription LIKE '%' || :text || '%' - OR fullContent LIKE '%' || :text || '%' + a.title LIKE '%' || :text || '%' + OR a.shortDescription LIKE '%' || :text || '%' + OR a.fullContent LIKE '%' || :text || '%' + ) + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun searchArticleWhenAll( - accountId: Int, text: String, sortAscending: Boolean = false + accountId: Int, text: String, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @@ -434,16 +502,28 @@ interface ArticleDao { @Transaction @Query( """ - SELECT feedId, COUNT(*) AS important - FROM article - WHERE isUnread = :isUnread - AND accountId = :accountId - GROUP BY feedId + SELECT a.feedId, COUNT(*) AS important + FROM article AS a + WHERE a.isUnread = :isUnread + AND a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) + GROUP BY a.feedId """ ) fun queryImportantCountWhenIsUnread( accountId: Int, isUnread: Boolean, + hideDuplicates: Boolean = false, ): Flow> @Transaction @@ -464,28 +544,50 @@ interface ArticleDao { @Transaction @Query( """ - SELECT feedId, COUNT(*) AS important - FROM article - WHERE accountId = :accountId - GROUP BY feedId + SELECT a.feedId, COUNT(*) AS important + FROM article AS a + WHERE a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) + GROUP BY a.feedId """ ) - fun queryImportantCountWhenIsAll(accountId: Int): + fun queryImportantCountWhenIsAll(accountId: Int, hideDuplicates: Boolean = false): Flow> @Transaction @Query( """ - SELECT * FROM article - WHERE accountId = :accountId + SELECT * FROM article AS a + WHERE a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun queryArticleWithFeedWhenIsAll( - accountId: Int, sortAscending: Boolean = false + accountId: Int, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -506,16 +608,27 @@ interface ArticleDao { @Transaction @Query( """ - SELECT * FROM article - WHERE isUnread = :isUnread - AND accountId = :accountId + SELECT * FROM article AS a + WHERE a.isUnread = :isUnread + AND a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun queryArticleWithFeedWhenIsUnread( - accountId: Int, isUnread: Boolean, sortAscending: Boolean = false + accountId: Int, isUnread: Boolean, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -530,13 +643,24 @@ interface ArticleDao { LEFT JOIN `group` AS c ON c.id = b.groupId WHERE c.id = :groupId AND a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) ORDER BY CASE WHEN :sortAscending = 1 THEN a.date END ASC, CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun queryArticleWithFeedByGroupIdWhenIsAll( - accountId: Int, groupId: String, sortAscending: Boolean = false + accountId: Int, groupId: String, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -574,28 +698,50 @@ interface ArticleDao { WHERE c.id = :groupId AND a.isUnread = :isUnread AND a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) ORDER BY CASE WHEN :sortAscending = 1 THEN a.date END ASC, CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun queryArticleWithFeedByGroupIdWhenIsUnread( - accountId: Int, groupId: String, isUnread: Boolean, sortAscending: Boolean = false + accountId: Int, groupId: String, isUnread: Boolean, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @Query( """ - SELECT * FROM article - WHERE feedId = :feedId - AND accountId = :accountId + SELECT * FROM article AS a + WHERE a.feedId = :feedId + AND a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun queryArticleWithFeedByFeedIdWhenIsAll( - accountId: Int, feedId: String, sortAscending: Boolean = false + accountId: Int, feedId: String, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource @Transaction @@ -617,17 +763,28 @@ interface ArticleDao { @Transaction @Query( """ - SELECT * FROM article - WHERE feedId = :feedId - AND isUnread = :isUnread - AND accountId = :accountId + SELECT * FROM article AS a + WHERE a.feedId = :feedId + AND a.isUnread = :isUnread + AND a.accountId = :accountId + AND ( + :hideDuplicates = 0 + OR a.title = '' + OR NOT EXISTS ( + SELECT 1 FROM article AS d + WHERE d.accountId = a.accountId + AND d.feedId = a.feedId + AND d.title = a.title + AND (d.date < a.date OR (d.date = a.date AND d.id < a.id)) + ) + ) ORDER BY - CASE WHEN :sortAscending = 1 THEN date END ASC, - CASE WHEN :sortAscending = 0 THEN date END DESC + CASE WHEN :sortAscending = 1 THEN a.date END ASC, + CASE WHEN :sortAscending = 0 THEN a.date END DESC """ ) fun queryArticleWithFeedByFeedIdWhenIsUnread( - accountId: Int, feedId: String, isUnread: Boolean, sortAscending: Boolean = false + accountId: Int, feedId: String, isUnread: Boolean, sortAscending: Boolean = false, hideDuplicates: Boolean = false ): PagingSource diff --git a/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt index 32f66c7ba..c0a14e49a 100644 --- a/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt @@ -220,6 +220,7 @@ abstract class AbstractRssRepository( isStarred: Boolean, isUnread: Boolean, sortAscending: Boolean = false, + hideDuplicates: Boolean = false, ): PagingSource { val accountId = accountService.getCurrentAccountId() Log.i( @@ -241,10 +242,15 @@ abstract class AbstractRssRepository( accountId, groupId, true, - sortAscending = sortAscending, + sortAscending, + hideDuplicates, ) - else -> articleDao.queryArticleWithFeedByGroupIdWhenIsAll(accountId, groupId) + else -> articleDao.queryArticleWithFeedByGroupIdWhenIsAll( + accountId, + groupId, + hideDuplicates = hideDuplicates, + ) } feedId != null -> @@ -261,10 +267,15 @@ abstract class AbstractRssRepository( accountId, feedId, true, - sortAscending = sortAscending, + sortAscending, + hideDuplicates, ) - else -> articleDao.queryArticleWithFeedByFeedIdWhenIsAll(accountId, feedId) + else -> articleDao.queryArticleWithFeedByFeedIdWhenIsAll( + accountId, + feedId, + hideDuplicates = hideDuplicates, + ) } else -> @@ -274,16 +285,24 @@ abstract class AbstractRssRepository( articleDao.queryArticleWithFeedWhenIsUnread( accountId, true, - sortAscending = sortAscending, + sortAscending, + hideDuplicates, ) - else -> articleDao.queryArticleWithFeedWhenIsAll(accountId) + else -> articleDao.queryArticleWithFeedWhenIsAll( + accountId, + hideDuplicates = hideDuplicates, + ) } } } @OptIn(ExperimentalCoroutinesApi::class) - fun pullImportant(isStarred: Boolean, isUnread: Boolean): Flow> { + fun pullImportant( + isStarred: Boolean, + isUnread: Boolean, + hideDuplicates: Boolean = false, + ): Flow> { val accountId = accountService.getCurrentAccountId() Log.i( "RLog", @@ -291,8 +310,8 @@ abstract class AbstractRssRepository( ) return when { isStarred -> articleDao.queryImportantCountWhenIsStarred(accountId, true) - isUnread -> articleDao.queryImportantCountWhenIsUnread(accountId, true) - else -> articleDao.queryImportantCountWhenIsAll(accountId) + isUnread -> articleDao.queryImportantCountWhenIsUnread(accountId, true, hideDuplicates) + else -> articleDao.queryImportantCountWhenIsAll(accountId, hideDuplicates) } } @@ -417,6 +436,7 @@ abstract class AbstractRssRepository( isStarred: Boolean, isUnread: Boolean, sortAscending: Boolean = false, + hideDuplicates: Boolean = false, ): PagingSource { val accountId = accountService.getCurrentAccountId() Log.i( @@ -441,9 +461,15 @@ abstract class AbstractRssRepository( groupId, true, sortAscending, + hideDuplicates, ) - else -> articleDao.searchArticleByGroupIdWhenAll(accountId, content, groupId) + else -> articleDao.searchArticleByGroupIdWhenAll( + accountId, + content, + groupId, + hideDuplicates = hideDuplicates, + ) } feedId != null -> @@ -463,9 +489,15 @@ abstract class AbstractRssRepository( feedId, true, sortAscending, + hideDuplicates, ) - else -> articleDao.searchArticleByFeedIdWhenAll(accountId, content, feedId) + else -> articleDao.searchArticleByFeedIdWhenAll( + accountId, + content, + feedId, + hideDuplicates = hideDuplicates, + ) } else -> @@ -477,9 +509,14 @@ abstract class AbstractRssRepository( content, true, sortAscending, + hideDuplicates, ) - else -> articleDao.searchArticleWhenAll(accountId, content) + else -> articleDao.searchArticleWhenAll( + accountId, + content, + hideDuplicates = hideDuplicates, + ) } } } diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/HideDuplicateArticlesPreference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/HideDuplicateArticlesPreference.kt new file mode 100644 index 000000000..9e3ebc7cc --- /dev/null +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/HideDuplicateArticlesPreference.kt @@ -0,0 +1,48 @@ +package me.ash.reader.infrastructure.preference + +import android.content.Context +import androidx.compose.runtime.compositionLocalOf +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import me.ash.reader.ui.ext.DataStoreKey +import me.ash.reader.ui.ext.DataStoreKey.Companion.hideDuplicateArticles +import me.ash.reader.ui.ext.dataStore +import me.ash.reader.ui.ext.put + +val LocalHideDuplicateArticles = + compositionLocalOf { HideDuplicateArticlesPreference.default } + +sealed class HideDuplicateArticlesPreference(val value: Boolean) : Preference() { + data object ON : HideDuplicateArticlesPreference(true) + data object OFF : HideDuplicateArticlesPreference(false) + + override fun put(context: Context, scope: CoroutineScope) { + scope.launch { + context.dataStore.put( + hideDuplicateArticles, + value + ) + } + } + + fun toggle(context: Context, scope: CoroutineScope) = scope.launch { + context.dataStore.put( + hideDuplicateArticles, + !value + ) + } + + companion object { + + val default = OFF + val values = listOf(ON, OFF) + + fun fromPreferences(preferences: Preferences) = + when (preferences[DataStoreKey.keys[hideDuplicateArticles]?.key as Preferences.Key]) { + true -> ON + false -> OFF + else -> default + } + } +} diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt index bdc2c057d..c048cb210 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt @@ -81,6 +81,7 @@ fun Preferences.toSettings(): Settings { swipeEndAction = SwipeEndActionPreference.fromPreferences(this), markAsReadOnScroll = MarkAsReadOnScrollPreference.fromPreferences(this), hideEmptyGroups = HideEmptyGroupsPreference.fromPreferences(this), + hideDuplicateArticles = HideDuplicateArticlesPreference.fromPreferences(this), pullToSwitchFeed = PullToLoadNextFeedPreference.fromPreference(this), pullToSwitchArticle = PullToSwitchArticlePreference.fromPreference(this), openLink = OpenLinkPreference.fromPreferences(this), diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt index af4f01012..9e3f3378c 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt @@ -74,6 +74,7 @@ data class Settings( val swipeEndAction: SwipeEndActionPreference = SwipeEndActionPreference.default, val markAsReadOnScroll: MarkAsReadOnScrollPreference = MarkAsReadOnScrollPreference.default, val hideEmptyGroups: HideEmptyGroupsPreference = HideEmptyGroupsPreference.default, + val hideDuplicateArticles: HideDuplicateArticlesPreference = HideDuplicateArticlesPreference.default, val pullToSwitchFeed: PullToLoadNextFeedPreference = PullToLoadNextFeedPreference.default, val pullToSwitchArticle: PullToSwitchArticlePreference = PullToSwitchArticlePreference.default, val openLink: OpenLinkPreference = OpenLinkPreference.default, diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt index dd14b72d3..38f9e166d 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/SettingsProvider.kt @@ -133,6 +133,7 @@ class SettingsProvider @Inject constructor( LocalArticleListSwipeEndAction provides settings.swipeEndAction, LocalMarkAsReadOnScroll provides settings.markAsReadOnScroll, LocalHideEmptyGroups provides settings.hideEmptyGroups, + LocalHideDuplicateArticles provides settings.hideDuplicateArticles, LocalPullToSwitchArticle provides settings.pullToSwitchArticle, LocalOpenLink provides settings.openLink, LocalOpenLinkSpecificBrowser provides settings.openLinkSpecificBrowser, diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index 7552b3d02..2b4937e33 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -192,6 +192,7 @@ sealed interface PreferencesKey { const val swipeEndAction = "swipeEndAction" const val markAsReadOnScroll = "markAsReadOnScroll" const val hideEmptyGroups = "hideEmptyGroups" + const val hideDuplicateArticles = "hideDuplicateArticles" const val pullToLoadNextFeed = "pullToLoadNextFeed" const val pullToSwitchArticle = "pullToSwitchArticle" const val openLink = "openLink" @@ -268,6 +269,7 @@ sealed interface PreferencesKey { IntKey(swipeEndAction), BooleanKey(markAsReadOnScroll), BooleanKey(hideEmptyGroups), + BooleanKey(hideDuplicateArticles), BooleanKey(pullToLoadNextFeed), BooleanKey(pullToSwitchArticle), IntKey(openLink), @@ -354,6 +356,7 @@ data class DataStoreKey(val key: Preferences.Key, val type: Class) { const val swipeEndAction = "swipeEndAction" const val markAsReadOnScroll = "markAsReadOnScroll" const val hideEmptyGroups = "hideEmptyGroups" + const val hideDuplicateArticles = "hideDuplicateArticles" const val pullToLoadNextFeed = "pullToLoadNextFeed" const val pullToSwitchArticle = "pullToSwitchArticle" const val openLink = "openLink" @@ -500,6 +503,8 @@ data class DataStoreKey(val key: Preferences.Key, val type: Class) { DataStoreKey(booleanPreferencesKey(markAsReadOnScroll), Boolean::class.java), hideEmptyGroups to DataStoreKey(booleanPreferencesKey(hideEmptyGroups), Boolean::class.java), + hideDuplicateArticles to + DataStoreKey(booleanPreferencesKey(hideDuplicateArticles), Boolean::class.java), pullToLoadNextFeed to DataStoreKey(booleanPreferencesKey(pullToLoadNextFeed), Boolean::class.java), pullToSwitchArticle to diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt index e151e33f3..962691415 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt @@ -17,7 +17,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -95,20 +97,27 @@ class FeedsViewModel @Inject constructor( .combine(accountFlow) { filter, account -> filter } + .combine( + settingsProvider.settingsFlow + .map { it.hideDuplicateArticles.value } + .distinctUntilChanged() + ) { filter, hideDuplicates -> + filter to hideDuplicates + } .collect { currentJob?.cancel() - currentJob = when (it) { - Filter.Unread -> pullUnreadFeeds() + currentJob = when (it.first) { + Filter.Unread -> pullUnreadFeeds(it.second) Filter.Starred -> pullStarredFeeds() - else -> pullAllFeeds() + else -> pullAllFeeds(it.second) } } } } - private fun pullAllFeeds(): Job { + private fun pullAllFeeds(hideDuplicates: Boolean): Job { val articleCountMapFlow = - rssService.get().pullImportant(isStarred = false, isUnread = false) + rssService.get().pullImportant(isStarred = false, isUnread = false, hideDuplicates = hideDuplicates) return viewModelScope.launch { launch { @@ -136,8 +145,9 @@ class FeedsViewModel @Inject constructor( } @OptIn(FlowPreview::class) - private fun pullUnreadFeeds(): Job { - val unreadCountMapFlow = rssService.get().pullImportant(isStarred = false, isUnread = true) + private fun pullUnreadFeeds(hideDuplicates: Boolean): Job { + val unreadCountMapFlow = + rssService.get().pullImportant(isStarred = false, isUnread = true, hideDuplicates = hideDuplicates) return viewModelScope.launch { diffMapHolder.diffMapSnapshotFlow diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt index 3fd197607..0f08109fa 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/interaction/InteractionPage.kt @@ -26,6 +26,7 @@ import me.ash.reader.infrastructure.preference.InitialPagePreference import me.ash.reader.infrastructure.preference.LocalArticleListSwipeEndAction import me.ash.reader.infrastructure.preference.LocalArticleListSwipeStartAction import me.ash.reader.infrastructure.preference.LocalHideEmptyGroups +import me.ash.reader.infrastructure.preference.LocalHideDuplicateArticles import me.ash.reader.infrastructure.preference.LocalInitialFilter import me.ash.reader.infrastructure.preference.LocalInitialPage import me.ash.reader.infrastructure.preference.LocalMarkAsReadOnScroll @@ -63,6 +64,7 @@ fun InteractionPage( val swipeToEndAction = LocalArticleListSwipeEndAction.current val markAsReadOnScroll = LocalMarkAsReadOnScroll.current val hideEmptyGroups = LocalHideEmptyGroups.current + val hideDuplicateArticles = LocalHideDuplicateArticles.current val sortUnreadArticles = LocalSortUnreadArticles.current val pullToSwitchArticle = LocalPullToSwitchArticle.current val openLink = LocalOpenLink.current @@ -177,6 +179,18 @@ fun InteractionPage( } } + SettingItem( + title = stringResource(R.string.hide_duplicate_articles), + desc = stringResource(R.string.hide_duplicate_articles_desc), + onClick = { + hideDuplicateArticles.toggle(context, scope) + }, + ) { + RYSwitch(activated = hideDuplicateArticles.value) { + hideDuplicateArticles.toggle(context, scope) + } + } + SettingItem( title = stringResource(R.string.pull_from_bottom), desc = pullToSwitchFeed.description(), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7f249f63..06ea21af1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,6 +348,8 @@ About Mark as read on scroll Hide empty groups + Hide duplicate articles + Only show the oldest article when the same feed has several with the same title Become a sponsor We build and maintain this free, open source app in our free time. If you enjoy it, please consider supporting us with a small donation! ☕️ Toolbars diff --git a/app/src/test/java/me/ash/reader/infrastructure/preference/HideDuplicateArticlesPreferenceTest.kt b/app/src/test/java/me/ash/reader/infrastructure/preference/HideDuplicateArticlesPreferenceTest.kt new file mode 100644 index 000000000..5d5672a05 --- /dev/null +++ b/app/src/test/java/me/ash/reader/infrastructure/preference/HideDuplicateArticlesPreferenceTest.kt @@ -0,0 +1,49 @@ +package me.ash.reader.infrastructure.preference + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.preferencesOf +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class HideDuplicateArticlesPreferenceTest { + + @Before + fun resolvePreferenceObjects() { + // Resolve the data objects first: under the unit-test classloader the + // companion's `default` is otherwise observed as null before class init settles. + HideDuplicateArticlesPreference.ON + HideDuplicateArticlesPreference.OFF + } + + @Test + fun defaultIsOff() { + Assert.assertEquals( + HideDuplicateArticlesPreference.OFF, + HideDuplicateArticlesPreference.default, + ) + Assert.assertFalse(HideDuplicateArticlesPreference.default.value) + } + + @Test + fun fromPreferencesMapsStoredValue() { + val key = booleanPreferencesKey("hideDuplicateArticles") + Assert.assertEquals( + HideDuplicateArticlesPreference.ON, + HideDuplicateArticlesPreference.fromPreferences(preferencesOf(key to true)), + ) + Assert.assertEquals( + HideDuplicateArticlesPreference.OFF, + HideDuplicateArticlesPreference.fromPreferences(preferencesOf(key to false)), + ) + } + + @Test + fun fromPreferencesFallsBackToOffWhenMissing() { + Assert.assertEquals( + HideDuplicateArticlesPreference.OFF, + HideDuplicateArticlesPreference.fromPreferences(emptyPreferences()), + ) + } +}