diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt index 685bc981f..c8db96401 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import org.kodein.di.DI @@ -276,6 +275,10 @@ class Repository(override val di: DI) : DIAware { sessionStore.setResumeTime(value) } + val showTitleUnreadCount = settingsStore.showTitleUnreadCount + + fun setShowTitleUnreadCount(value: Boolean) = settingsStore.setShowTitleUnreadCount(value) + /** * Returns true if the latest sync timestamp is within the last 10 seconds */ @@ -416,44 +419,46 @@ class Repository(override val di: DI) : DIAware { fun getScreenTitleForFeedOrTag( feedId: Long, tag: String, - ) = flow { - emit( - ScreenTitle( - title = - when { - feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) - tag.isNotBlank() -> tag - else -> null - }, - type = - when (feedId) { - ID_UNSET -> FeedType.TAG - ID_ALL_FEEDS -> FeedType.ALL_FEEDS - ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES - else -> FeedType.FEED - }, - ), + ) = getUnreadCount(feedId).mapLatest { unreadCount -> + ScreenTitle( + title = + when { + feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) + tag.isNotBlank() -> tag + else -> null + }, + type = + when (feedId) { + ID_UNSET -> FeedType.TAG + ID_ALL_FEEDS -> FeedType.ALL_FEEDS + ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES + else -> FeedType.FEED + }, + unreadCount = unreadCount, ) } @OptIn(ExperimentalCoroutinesApi::class) fun getScreenTitleForCurrentFeedOrTag(): Flow = - currentFeedAndTag.mapLatest { (feedId, tag) -> - ScreenTitle( - title = - when { - feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) - tag.isNotBlank() -> tag - else -> null - }, - type = - when (feedId) { - ID_UNSET -> FeedType.TAG - ID_ALL_FEEDS -> FeedType.ALL_FEEDS - ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES - else -> FeedType.FEED - }, - ) + currentFeedAndTag.flatMapLatest { (feedId, tag) -> + getUnreadCount(feedId).mapLatest { unreadCount -> + ScreenTitle( + title = + when { + feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) + tag.isNotBlank() -> tag + else -> null + }, + type = + when (feedId) { + ID_UNSET -> FeedType.TAG + ID_ALL_FEEDS -> FeedType.ALL_FEEDS + ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES + else -> FeedType.FEED + }, + unreadCount = unreadCount, + ) + } } suspend fun deleteFeeds(feedIds: List) { @@ -834,6 +839,7 @@ private data class FeedListArgs( data class ScreenTitle( val title: String?, val type: FeedType, + val unreadCount: Int, ) enum class FeedType { diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt index 6c55b57a4..21d6bd074 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt @@ -481,6 +481,14 @@ class SettingsStore(override val di: DI) : DIAware { } } + private val _showTitleUnreadCount = MutableStateFlow(sp.getBoolean(PREF_SHOW_TITLE_UNREAD_COUNT, false)) + val showTitleUnreadCount = _showTitleUnreadCount.asStateFlow() + + fun setShowTitleUnreadCount(value: Boolean) { + _showTitleUnreadCount.value = value + sp.edit().putBoolean(PREF_SHOW_TITLE_UNREAD_COUNT, value).apply() + } + fun getAllSettings(): Map { val all = sp.all ?: emptyMap() @@ -578,6 +586,11 @@ const val PREF_LIST_SHOW_READING_TIME = "pref_show_reading_time" */ const val PREF_READALOUD_USE_DETECT_LANGUAGE = "pref_readaloud_detect_lang" +/** + * Appearance settings + */ +const val PREF_SHOW_TITLE_UNREAD_COUNT = "pref_show_title_unread_count" + /** * Used for OPML Import/Export. Please add new (only) user configurable settings here */ diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt index 6d37a21fe..9e90e7370 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt @@ -996,10 +996,19 @@ fun FeedScreen( scrollBehavior = scrollBehavior, title = when (viewState.feedScreenTitle.type) { - FeedType.FEED -> viewState.feedScreenTitle.title - FeedType.TAG -> viewState.feedScreenTitle.title + FeedType.FEED, FeedType.TAG -> viewState.feedScreenTitle.title FeedType.SAVED_ARTICLES -> stringResource(id = R.string.saved_articles) FeedType.ALL_FEEDS -> stringResource(id = R.string.all_feeds) + }.let { title -> + if (viewState.feedScreenTitle.type == FeedType.SAVED_ARTICLES || + !viewState.showTitleUnreadCount || + viewState.feedScreenTitle.unreadCount == 0 || + title == null + ) { + title + } else { + title + " ${stringResource(id = R.string.title_unread_count, viewState.feedScreenTitle.unreadCount)}" + } } ?: "", navigationIcon = { IconButton( diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt index e1382bca1..c38d42b48 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt @@ -97,7 +97,7 @@ class FeedArticleViewModel( .stateIn( viewModelScope, SharingStarted.Eagerly, - ScreenTitle("", FeedType.ALL_FEEDS), + ScreenTitle("", FeedType.ALL_FEEDS, 0), ) private val visibleFeeds: StateFlow> = @@ -308,6 +308,7 @@ class FeedArticleViewModel( repository.showOnlyTitle, repository.showReadingTime, repository.syncWorkerRunning, + repository.showTitleUnreadCount, ) { params: Array -> val article = params[15] as Article @@ -374,6 +375,7 @@ class FeedArticleViewModel( }, image = article.image, articleContent = parseArticleContent(article, textToDisplay), + showTitleUnreadCount = params[28] as Boolean, ) } .stateIn( @@ -608,6 +610,7 @@ interface FeedScreenViewState { val showReadingTime: Boolean val filter: FeedListFilter val showFilterMenu: Boolean + val showTitleUnreadCount: Boolean } @Immutable @@ -702,7 +705,7 @@ data class FeedArticleScreenViewState( override val currentTheme: ThemeOptions = ThemeOptions.SYSTEM, override val currentlySyncing: Boolean = false, // Defaults to empty string to avoid rendering until loading complete - override val feedScreenTitle: ScreenTitle = ScreenTitle("", FeedType.FEED), + override val feedScreenTitle: ScreenTitle = ScreenTitle("", FeedType.FEED, 0), override val visibleFeeds: List = emptyList(), override val feedItemStyle: FeedItemStyle = FeedItemStyle.CARD, override val expandedTags: Set = emptySet(), @@ -739,6 +742,7 @@ data class FeedArticleScreenViewState( override val wordCount: Int = 0, override val image: ThumbnailImage? = null, override val articleContent: LinearArticle = LinearArticle(elements = emptyList()), + override val showTitleUnreadCount: Boolean = false, ) : FeedScreenViewState, ArticleScreenViewState sealed class TSSError diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt index 3202f2d52..58613d33c 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt @@ -201,6 +201,8 @@ fun SettingsScreen( onOpenAdjacent = settingsViewModel::setIsOpenAdjacent, showReadingTime = viewState.showReadingTime, onShowReadingTimeChange = settingsViewModel::setShowReadingTime, + showTitleUnreadCount = viewState.showTitleUnreadCount, + onShowTitleUnreadCountChange = settingsViewModel::setShowTitleUnreadCount, onStartActivity = { intent -> activityLauncher.startActivity(false, intent) }, @@ -268,6 +270,8 @@ private fun SettingsScreenPreview() { onOpenAdjacent = {}, showReadingTime = false, onShowReadingTimeChange = {}, + showTitleUnreadCount = false, + onShowTitleUnreadCountChange = {}, onStartActivity = {}, modifier = Modifier, ) @@ -329,6 +333,8 @@ fun SettingsList( onOpenAdjacent: (Boolean) -> Unit, showReadingTime: Boolean, onShowReadingTimeChange: (Boolean) -> Unit, + showTitleUnreadCount: Boolean, + onShowTitleUnreadCountChange: (Boolean) -> Unit, onStartActivity: (intent: Intent) -> Unit, modifier: Modifier = Modifier, ) { @@ -591,6 +597,12 @@ fun SettingsList( onCheckedChange = onShowReadingTimeChange, ) + SwitchSetting( + title = stringResource(id = R.string.show_title_unread_count), + checked = showTitleUnreadCount, + onCheckedChange = onShowTitleUnreadCountChange, + ) + HorizontalDivider(modifier = Modifier.width(dimens.maxContentWidth)) GroupTitle { innerModifier -> diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt index 8d6b3b806..23e15f951 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt @@ -147,6 +147,10 @@ class SettingsViewModel(di: DI) : DIAwareViewModel(di) { repository.setShowReadingTime(value) } + fun setShowTitleUnreadCount(value: Boolean) { + repository.setShowTitleUnreadCount(value) + } + @OptIn(ExperimentalCoroutinesApi::class) private val immutableFeedsSettings = repository.feedNotificationSettings @@ -199,6 +203,7 @@ class SettingsViewModel(di: DI) : DIAwareViewModel(di) { repository.showOnlyTitle, repository.isOpenAdjacent, repository.showReadingTime, + repository.showTitleUnreadCount, ) { params: Array -> @Suppress("UNCHECKED_CAST") SettingsViewState( @@ -228,6 +233,7 @@ class SettingsViewModel(di: DI) : DIAwareViewModel(di) { showOnlyTitle = params[23] as Boolean, isOpenAdjacent = params[24] as Boolean, showReadingTime = params[25] as Boolean, + showTitleUnreadCount = params[26] as Boolean, ) }.collect { _viewState.value = it @@ -269,6 +275,7 @@ data class SettingsViewState( val showOnlyTitle: Boolean = false, val isOpenAdjacent: Boolean = true, val showReadingTime: Boolean = false, + val showTitleUnreadCount: Boolean = false, ) data class UIFeedSettings( diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bddb0cf20..cbbb4b837 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -248,4 +248,5 @@ Doppelte Artikel überspringen Artikel mit Links oder Titeln, die mit bestehenden Artikeln identisch sind, werden ignoriert Kompakte Kartei - \ No newline at end of file + Zeige Anzahl ungelesener Artikel im Titel + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4c166951..822772809 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -261,5 +261,7 @@ Close menu Skip duplicate articles Articles with links or titles identical to existing articles are ignored - Touch to play audio + Touch to play audio + (%1$d) + Show unread article count in title diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt index 9082ebb51..b13129bbf 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt @@ -91,6 +91,8 @@ class RepositoryTest : DIAware { every { settingsStore.syncOnlyOnWifi } returns MutableStateFlow(false) every { settingsStore.addedFeederNews } returns MutableStateFlow(true) every { settingsStore.minReadTime } returns MutableStateFlow(Instant.EPOCH) + + every { feedItemStore.getFeedItemCountRaw(any(), any(), any(), any()) } returns flowOf(0) } @Test @@ -286,7 +288,7 @@ class RepositoryTest : DIAware { repository.getScreenTitleForFeedOrTag(ID_ALL_FEEDS, "").toList().first() } - assertEquals(ScreenTitle(title = null, type = FeedType.ALL_FEEDS), result) + assertEquals(ScreenTitle(title = null, type = FeedType.ALL_FEEDS, unreadCount = 0), result) } @Test @@ -296,7 +298,7 @@ class RepositoryTest : DIAware { repository.getScreenTitleForFeedOrTag(ID_SAVED_ARTICLES, "").toList().first() } - assertEquals(ScreenTitle(title = null, type = FeedType.SAVED_ARTICLES), result) + assertEquals(ScreenTitle(title = null, type = FeedType.SAVED_ARTICLES, unreadCount = 0), result) } @Test @@ -306,7 +308,7 @@ class RepositoryTest : DIAware { repository.getScreenTitleForFeedOrTag(ID_UNSET, "fwr").toList().first() } - assertEquals(ScreenTitle(title = "fwr", type = FeedType.TAG), result) + assertEquals(ScreenTitle(title = "fwr", type = FeedType.TAG, unreadCount = 0), result) } @Test @@ -318,7 +320,7 @@ class RepositoryTest : DIAware { repository.getScreenTitleForFeedOrTag(5L, "fwr").toList().first() } - assertEquals(ScreenTitle(title = "floppa", type = FeedType.FEED), result) + assertEquals(ScreenTitle(title = "floppa", type = FeedType.FEED, unreadCount = 0), result) coVerify { feedStore.getDisplayTitle(5L) diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt index 1401accbc..d443d9329 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt @@ -390,4 +390,15 @@ class SettingsStoreTest : DIAware { val allSettings = store.getAllSettings() assertEquals(1, allSettings.size) } + + @Test + fun showTitleUnreadCount() { + store.setShowTitleUnreadCount(true) + + verify { + sp.edit().putBoolean(PREF_SHOW_TITLE_UNREAD_COUNT, true).apply() + } + + assertEquals(true, store.showTitleUnreadCount.value) + } }