) =
+ featuredPosts
+ .map { post ->
+ post.copy(
+ seedColor =
+ withContext(dispatchersProvider.default) {
+ seedColorExtractor.calculateSeedColor(post.postWithMetadata.imageUrl)
+ }
+ )
+ }
+ .toImmutableList()
+
+ private fun getPostsAfter(postsType: PostsType) =
+ when (postsType) {
+ PostsType.ALL,
+ PostsType.UNREAD -> Instant.DISTANT_PAST
+ PostsType.TODAY -> {
+ getTodayStartInstant()
+ }
+ PostsType.LAST_24_HOURS -> {
+ getLast24HourStart()
+ }
+ }
+
+ private fun getUnreadOnly(postsType: PostsType) =
+ when (postsType) {
+ PostsType.ALL,
+ PostsType.TODAY,
+ PostsType.LAST_24_HOURS -> null
+ PostsType.UNREAD -> true
+ }
+
private fun loadFeaturedPostsItems(
activeSource: Source?,
unreadOnly: Boolean?,
@@ -400,15 +390,11 @@ class HomePresenter(
unreadOnly = unreadOnly,
after = postsAfter
)
- .map { featuredPosts ->
+ .mapLatest { featuredPosts ->
featuredPosts.map { postWithMetadata ->
- val seedColor =
- withContext(dispatchersProvider.default) {
- seedColorExtractor.cached(postWithMetadata.imageUrl)
- }
FeaturedPostItem(
postWithMetadata = postWithMetadata,
- seedColor = seedColor,
+ seedColor = null,
)
}
}
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt
index 6921c399c..5e7eab1f8 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedSection.kt
@@ -186,26 +186,25 @@ internal fun FeaturedSection(
val featuredPost = featuredPosts.getOrNull(page)
if (featuredPost != null) {
Box {
- if (useDarkTheme) {
- FeaturedSectionBackground(
- modifier =
- Modifier.matchParentSize().layout { measurable, constraints ->
- val topPadding = contentPadding.calculateTopPadding().roundToPx()
- val horizontalContentPadding =
- contentPadding.calculateStartPadding(layoutDirection) +
- contentPadding.calculateEndPadding(layoutDirection)
- val fullWidth = constraints.maxWidth + horizontalContentPadding.roundToPx()
- val placeable = measurable.measure(constraints.copy(maxWidth = fullWidth))
+ FeaturedSectionBackground(
+ modifier =
+ Modifier.matchParentSize().layout { measurable, constraints ->
+ val topPadding = contentPadding.calculateTopPadding().roundToPx()
+ val horizontalContentPadding =
+ contentPadding.calculateStartPadding(layoutDirection) +
+ contentPadding.calculateEndPadding(layoutDirection)
+ val fullWidth = constraints.maxWidth + horizontalContentPadding.roundToPx()
+ val placeable = measurable.measure(constraints.copy(maxWidth = fullWidth))
- layout(placeable.width, placeable.height) {
- placeable.place(0, topPadding.unaryMinus())
- }
- },
- state = pagerState,
- page = page,
- featuredPost = featuredPost,
- )
- }
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, topPadding.unaryMinus())
+ }
+ },
+ state = pagerState,
+ page = page,
+ featuredPost = featuredPost,
+ useDarkTheme = useDarkTheme,
+ )
val postWithMetadata = featuredPost.postWithMetadata
FeaturedPostItem(
@@ -231,21 +230,27 @@ private fun FeaturedSectionBackground(
state: PagerState,
page: Int,
featuredPost: FeaturedPostItem,
+ useDarkTheme: Boolean,
modifier: Modifier = Modifier,
) {
Box(modifier) {
val gradientOverlayModifier =
Modifier.drawWithCache {
+ val gradientColor = if (useDarkTheme) Color.Black else Color.White
val radialGradient =
Brush.radialGradient(
colors =
- listOf(Color.Black, Color.Black.copy(alpha = 0.0f), Color.Black.copy(alpha = 0.0f)),
+ listOf(
+ gradientColor,
+ gradientColor.copy(alpha = 0.0f),
+ gradientColor.copy(alpha = 0.0f)
+ ),
center = Offset(x = this.size.width, y = 40f)
)
val linearGradient =
Brush.verticalGradient(
- colors = listOf(Color.Black, Color.Black.copy(alpha = 0.0f)),
+ colors = listOf(gradientColor, gradientColor.copy(alpha = 0.0f)),
)
onDrawWithContent {
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeTopAppBar.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeTopAppBar.kt
index 4ad8a0d23..fe298f868 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeTopAppBar.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeTopAppBar.kt
@@ -62,7 +62,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import dev.sasikanth.rss.reader.components.DropdownMenu
import dev.sasikanth.rss.reader.components.DropdownMenuItem
-import dev.sasikanth.rss.reader.components.image.FeedFavIcon
+import dev.sasikanth.rss.reader.components.image.FeedIcon
import dev.sasikanth.rss.reader.core.model.local.Feed
import dev.sasikanth.rss.reader.core.model.local.FeedGroup
import dev.sasikanth.rss.reader.core.model.local.PostsType
@@ -73,6 +73,7 @@ import dev.sasikanth.rss.reader.resources.icons.Tune
import dev.sasikanth.rss.reader.resources.icons.TwineIcons
import dev.sasikanth.rss.reader.resources.strings.LocalStrings
import dev.sasikanth.rss.reader.ui.AppTheme
+import dev.sasikanth.rss.reader.utils.LocalShowFeedFavIconSetting
private const val APP_BAR_OPAQUE_THRESHOLD = 200f
@@ -189,24 +190,26 @@ private fun SourceIcon(source: Source?, modifier: Modifier = Modifier) {
Spacer(Modifier.requiredWidth(16.dp))
}
+ val showFeedFavIcon = LocalShowFeedFavIconSetting.current
when (source) {
is FeedGroup -> {
+ val icons = if (showFeedFavIcon) source.feedHomepageLinks else source.feedIconLinks
val iconSize =
- if (source.feedHomepageLinks.size > 2) {
+ if (icons.size > 2) {
18.dp
} else {
20.dp
}
val iconSpacing =
- if (source.feedHomepageLinks.size > 2) {
+ if (icons.size > 2) {
4.dp
} else {
0.dp
}
FeedGroupIconGrid(
- icons = source.feedHomepageLinks,
+ icons = icons,
iconSize = iconSize,
iconShape = RoundedCornerShape(percent = 30),
horizontalArrangement = Arrangement.spacedBy(iconSpacing),
@@ -214,8 +217,9 @@ private fun SourceIcon(source: Source?, modifier: Modifier = Modifier) {
)
}
is Feed -> {
- FeedFavIcon(
- url = source.homepageLink,
+ val icon = if (showFeedFavIcon) source.homepageLink else source.icon
+ FeedIcon(
+ url = icon,
contentDescription = null,
modifier = Modifier.clip(MaterialTheme.shapes.small).requiredSize(24.dp)
)
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt
index 1311a6e4d..688edc70c 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostList.kt
@@ -140,12 +140,14 @@ fun PostListItem(
onPostCommentsClick: () -> Unit,
onPostSourceClick: () -> Unit,
togglePostReadClick: () -> Unit,
+ modifier: Modifier = Modifier,
reduceReadItemAlpha: Boolean = false,
postMetadataConfig: PostMetadataConfig = PostMetadataConfig.DEFAULT,
) {
Column(
modifier =
- Modifier.clickable(onClick = onClick)
+ Modifier.then(modifier)
+ .clickable(onClick = onClick)
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))
.padding(postListPadding)
.alpha(
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt
index 46706dcd6..fed2609cc 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ReaderPresenter.kt
@@ -142,7 +142,8 @@ class ReaderPresenter(
)
}
- if (feed.alwaysFetchSourceArticle) {
+ val hasContent = post.description.isNotBlank() || post.rawContent.isNullOrBlank().not()
+ if (feed.alwaysFetchSourceArticle || hasContent.not()) {
loadSourceArticle()
} else {
loadRssContent()
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt
index 4439d3dea..44d7c51dd 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsEvent.kt
@@ -29,6 +29,10 @@ sealed interface SettingsEvent {
data class ToggleShowReaderView(val value: Boolean) : SettingsEvent
+ data class ToggleAutoSync(val value: Boolean) : SettingsEvent
+
+ data class ToggleShowFeedFavIcon(val value: Boolean) : SettingsEvent
+
data object AboutClicked : SettingsEvent
data object ImportOpmlClicked : SettingsEvent
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt
index 458315f32..4a2ef8c59 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsPresenter.kt
@@ -110,21 +110,24 @@ class SettingsPresenter(
settingsRepository.postsDeletionPeriod,
settingsRepository.showReaderView,
settingsRepository.appThemeMode,
- rssRepository.hasFeeds()
+ settingsRepository.enableAutoSync,
+ settingsRepository.showFeedFavIcon
) {
browserType,
showUnreadPostsCount,
postsDeletionPeriod,
showReaderView,
appThemeMode,
- hasFeeds ->
+ enableAutoSync,
+ showFeedFavIcon ->
Settings(
browserType = browserType,
showUnreadPostsCount = showUnreadPostsCount,
- hasFeeds = hasFeeds,
postsDeletionPeriod = postsDeletionPeriod,
showReaderView = showReaderView,
appThemeMode = appThemeMode,
+ enableAutoSync = enableAutoSync,
+ showFeedFavIcon = showFeedFavIcon,
)
}
.onEach { settings ->
@@ -132,15 +135,21 @@ class SettingsPresenter(
it.copy(
browserType = settings.browserType,
showUnreadPostsCount = settings.showUnreadPostsCount,
- hasFeeds = settings.hasFeeds,
postsDeletionPeriod = settings.postsDeletionPeriod,
showReaderView = settings.showReaderView,
appThemeMode = settings.appThemeMode,
+ enableAutoSync = settings.enableAutoSync,
+ showFeedFavIcon = settings.showFeedFavIcon,
)
}
}
.launchIn(coroutineScope)
+ rssRepository
+ .hasFeeds()
+ .onEach { hasFeeds -> _state.update { it.copy(hasFeeds = hasFeeds) } }
+ .launchIn(coroutineScope)
+
opmlManager.result
.onEach { result -> _state.update { it.copy(opmlResult = result) } }
.launchIn(coroutineScope)
@@ -154,6 +163,8 @@ class SettingsPresenter(
is SettingsEvent.UpdateBrowserType -> updateBrowserType(event.browserType)
is SettingsEvent.ToggleShowUnreadPostsCount -> toggleShowUnreadPostsCount(event.value)
is SettingsEvent.ToggleShowReaderView -> toggleShowReaderView(event.value)
+ is SettingsEvent.ToggleAutoSync -> toggleAutoSync(event.value)
+ is SettingsEvent.ToggleShowFeedFavIcon -> toggleShowFeedFavIcon(event.value)
SettingsEvent.AboutClicked -> {
// no-op
}
@@ -165,6 +176,14 @@ class SettingsPresenter(
}
}
+ private fun toggleShowFeedFavIcon(value: Boolean) {
+ coroutineScope.launch { settingsRepository.toggleShowFeedFavIcon(value) }
+ }
+
+ private fun toggleAutoSync(value: Boolean) {
+ coroutineScope.launch { settingsRepository.toggleAutoSync(value) }
+ }
+
private fun onAppThemeModeChanged(appThemeMode: AppThemeMode) {
coroutineScope.launch { settingsRepository.updateAppTheme(appThemeMode) }
}
@@ -206,8 +225,9 @@ class SettingsPresenter(
private data class Settings(
val browserType: BrowserType,
val showUnreadPostsCount: Boolean,
- val hasFeeds: Boolean,
val postsDeletionPeriod: Period,
val showReaderView: Boolean,
val appThemeMode: AppThemeMode,
+ val enableAutoSync: Boolean,
+ val showFeedFavIcon: Boolean,
)
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt
index 143b80458..68a40b4bb 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/SettingsState.kt
@@ -32,6 +32,8 @@ internal data class SettingsState(
val postsDeletionPeriod: Period?,
val showReaderView: Boolean,
val appThemeMode: AppThemeMode,
+ val enableAutoSync: Boolean,
+ val showFeedFavIcon: Boolean,
) {
companion object {
@@ -46,6 +48,8 @@ internal data class SettingsState(
postsDeletionPeriod = null,
showReaderView = false,
appThemeMode = AppThemeMode.Auto,
+ enableAutoSync = true,
+ showFeedFavIcon = true,
)
}
}
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt
index 73712ee3b..254f57e32 100644
--- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt
@@ -17,7 +17,6 @@ package dev.sasikanth.rss.reader.settings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -100,11 +99,6 @@ internal fun SettingsScreen(
val state by settingsPresenter.state.collectAsState()
val layoutDirection = LocalLayoutDirection.current
val linkHandler = LocalLinkHandler.current
- val isSystemInDarkMode =
- when (state.appThemeMode) {
- AppThemeMode.Dark -> true
- else -> isSystemInDarkTheme()
- }
Scaffold(
modifier = modifier,
@@ -221,6 +215,28 @@ internal fun SettingsScreen(
item { Divider(24.dp) }
+ item {
+ AutoSyncSettingItem(
+ enableAutoSync = state.enableAutoSync,
+ onValueChanged = { newValue ->
+ settingsPresenter.dispatch(SettingsEvent.ToggleAutoSync(newValue))
+ }
+ )
+ }
+
+ item { Divider(24.dp) }
+
+ item {
+ ShowFeedFavIconSettingItem(
+ showFeedFavIcon = state.showFeedFavIcon,
+ onValueChanged = { newValue ->
+ settingsPresenter.dispatch(SettingsEvent.ToggleShowFeedFavIcon(newValue))
+ }
+ )
+ }
+
+ item { Divider(24.dp) }
+
item {
PostsDeletionPeriodSettingItem(
postsDeletionPeriod = state.postsDeletionPeriod,
@@ -399,6 +415,83 @@ private fun PostsDeletionPeriodSettingItem(
}
}
+@Composable
+private fun ShowFeedFavIconSettingItem(
+ showFeedFavIcon: Boolean,
+ onValueChanged: (Boolean) -> Unit
+) {
+ var checked by remember(showFeedFavIcon) { mutableStateOf(showFeedFavIcon) }
+ Box(
+ modifier =
+ Modifier.clickable {
+ checked = !checked
+ onValueChanged(!showFeedFavIcon)
+ }
+ ) {
+ Row(
+ modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 24.dp, bottom = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ LocalStrings.current.showFeedFavIconTitle,
+ style = MaterialTheme.typography.titleMedium,
+ color = AppTheme.colorScheme.textEmphasisHigh
+ )
+ Text(
+ LocalStrings.current.showFeedFavIconDesc,
+ style = MaterialTheme.typography.labelLarge,
+ color = AppTheme.colorScheme.textEmphasisMed
+ )
+ }
+
+ Spacer(Modifier.width(16.dp))
+
+ Switch(
+ checked = checked,
+ onCheckedChange = { checked -> onValueChanged(checked) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun AutoSyncSettingItem(enableAutoSync: Boolean, onValueChanged: (Boolean) -> Unit) {
+ var checked by remember(enableAutoSync) { mutableStateOf(enableAutoSync) }
+ Box(
+ modifier =
+ Modifier.clickable {
+ checked = !checked
+ onValueChanged(!enableAutoSync)
+ }
+ ) {
+ Row(
+ modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 24.dp, bottom = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ LocalStrings.current.enableAutoSyncTitle,
+ style = MaterialTheme.typography.titleMedium,
+ color = AppTheme.colorScheme.textEmphasisHigh
+ )
+ Text(
+ LocalStrings.current.enableAutoSyncDesc,
+ style = MaterialTheme.typography.labelLarge,
+ color = AppTheme.colorScheme.textEmphasisMed
+ )
+ }
+
+ Spacer(Modifier.width(16.dp))
+
+ Switch(
+ checked = checked,
+ onCheckedChange = { checked -> onValueChanged(checked) },
+ )
+ }
+ }
+}
+
@Composable
private fun UnreadPostsCountSettingItem(
showUnreadCountEnabled: Boolean,
diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/LocalShowFeedFavIconSetting.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/LocalShowFeedFavIconSetting.kt
new file mode 100644
index 000000000..106d904ba
--- /dev/null
+++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/LocalShowFeedFavIconSetting.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 Sasikanth Miriyampalli
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dev.sasikanth.rss.reader.utils
+
+import androidx.compose.runtime.compositionLocalOf
+
+internal val LocalShowFeedFavIconSetting = compositionLocalOf { true }
diff --git a/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/FeedParserTest.kt b/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/FeedParserTest.kt
index 0d3324697..89f08a743 100644
--- a/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/FeedParserTest.kt
+++ b/shared/src/commonTest/kotlin/dev/sasikanth/rss/reader/FeedParserTest.kt
@@ -96,7 +96,7 @@ class FeedParserTest {
link = "https://example.com/post-with-relative-image",
description = "Relative image post description.",
rawContent = "Relative image post description.",
- imageUrl = "https://example.com/relative-media-url",
+ imageUrl = "http://example.com/relative-media-url",
date = 1685005200000,
commentsLink = null
),
@@ -228,7 +228,7 @@ class FeedParserTest {
Post summary with an image.
"""
.trimIndent(),
- imageUrl = "https://example.com/resources/image.jpg",
+ imageUrl = "http://example.com/resources/image.jpg",
date = 1685008800000,
commentsLink = null
),