From 33ef70fc43ea7e50e9ac64d44aea5fe4594ce231 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 16:09:39 +0530 Subject: [PATCH 01/21] Add class to extract seed color from a image URL --- .../rss/reader/ui/SeedColorExtractor.kt | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt new file mode 100644 index 000000000..7f4fc6ac9 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024 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.ui + +import androidx.collection.lruCache +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.annotation.ExperimentalCoilApi +import coil3.request.ImageRequest +import dev.sasikanth.material.color.utilities.quantize.QuantizerCelebi +import dev.sasikanth.material.color.utilities.score.Score +import dev.sasikanth.rss.reader.utils.toComposeImageBitmap +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.suspendCancellableCoroutine +import me.tatarka.inject.annotations.Inject + +@Inject +class SeedColorExtractor( + private val imageLoader: Lazy, + private val platformContext: Lazy, +) { + private val cache = lruCache(100) + + @OptIn(ExperimentalCoilApi::class) + suspend fun calculateSeedColor(url: String): Color { + val cached = cache[url] + if (cached != null) { + return cached + } + + val bitmap = suspendCancellableCoroutine { cont -> + val request = + ImageRequest.Builder(platformContext.value) + .data(url) + .size(DEFAULT_REQUEST_SIZE) + .target( + onSuccess = { result -> + cont.resume(result.toComposeImageBitmap(platformContext.value)) + }, + onError = { cont.resumeWithException(IllegalArgumentException()) }, + ) + .build() + + imageLoader.value.enqueue(request) + } + + return bitmap.seedColor().also { cache.put(url, it) } + } + + private fun ImageBitmap.seedColor(): Color { + val bitmapPixels = IntArray(width * height) + readPixels(buffer = bitmapPixels) + + return Color(Score.score(QuantizerCelebi.quantize(bitmapPixels, maxColors = 128)).first()) + } + + private companion object { + const val DEFAULT_REQUEST_SIZE = 64 + } +} From 3f5bc720904286231a1d604f8db5b2433c2fd62a Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 16:20:31 +0530 Subject: [PATCH 02/21] Enable automatic UI user interface style for iOS --- iosApp/iosApp/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index af8b50250..aacb7e3db 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -68,6 +68,6 @@ UIInterfaceOrientationLandscapeRight UIUserInterfaceStyle - Dark + Automatic From 71b9cfd5dbb245884a9af9832c79062655db3445 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 16:21:15 +0530 Subject: [PATCH 03/21] Add support for storing app theme mode in preferences --- .../data/repository/SettingsRepository.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/data/src/commonMain/kotlin/dev/sasikanth/rss/reader/data/repository/SettingsRepository.kt b/core/data/src/commonMain/kotlin/dev/sasikanth/rss/reader/data/repository/SettingsRepository.kt index 59056a102..e493bb6ce 100644 --- a/core/data/src/commonMain/kotlin/dev/sasikanth/rss/reader/data/repository/SettingsRepository.kt +++ b/core/data/src/commonMain/kotlin/dev/sasikanth/rss/reader/data/repository/SettingsRepository.kt @@ -40,6 +40,7 @@ class SettingsRepository(private val dataStore: DataStore) { private val showReaderViewKey = booleanPreferencesKey("pref_show_reader_view") private val feedsViewModeKey = stringPreferencesKey("pref_feeds_view_mode") private val feedsSortOrderKey = stringPreferencesKey("pref_feeds_sort_order") + private val appThemeModeKey = stringPreferencesKey("pref_app_theme_mode") val browserType: Flow = dataStore.data.map { preferences -> @@ -73,6 +74,11 @@ class SettingsRepository(private val dataStore: DataStore) { mapToFeedsOrderBy(preferences[feedsSortOrderKey]) ?: FeedsOrderBy.Latest } + val appThemeMode: Flow = + dataStore.data.map { preferences -> + mapToAppThemeMode(preferences[appThemeModeKey]) ?: AppThemeMode.Auto + } + suspend fun updateFeedsSortOrder(value: FeedsOrderBy) { dataStore.edit { preferences -> preferences[feedsSortOrderKey] = value.name } } @@ -109,6 +115,15 @@ class SettingsRepository(private val dataStore: DataStore) { dataStore.edit { preferences -> preferences[feedsViewModeKey] = value.name } } + suspend fun updateAppTheme(value: AppThemeMode) { + dataStore.edit { preferences -> preferences[appThemeModeKey] = value.name } + } + + private fun mapToAppThemeMode(pref: String?): AppThemeMode? { + if (pref.isNullOrBlank()) return null + return AppThemeMode.valueOf(pref) + } + private fun mapToFeedsOrderBy(pref: String?): FeedsOrderBy? { if (pref.isNullOrBlank()) return null return FeedsOrderBy.valueOf(pref) @@ -135,6 +150,12 @@ class SettingsRepository(private val dataStore: DataStore) { } } +enum class AppThemeMode { + Light, + Dark, + Auto +} + enum class BrowserType { Default, InApp From a911c31c730d419944f8cfc06e8b233089a44245 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 19:55:13 +0530 Subject: [PATCH 04/21] Change seed color extraction to return color int Also removed caching support, since we will add the seed color the featured post item UI model --- .../rss/reader/ui/SeedColorExtractor.kt | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt index 7f4fc6ac9..f053d6a14 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt @@ -16,8 +16,6 @@ package dev.sasikanth.rss.reader.ui -import androidx.collection.lruCache -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import coil3.ImageLoader import coil3.PlatformContext @@ -36,14 +34,9 @@ class SeedColorExtractor( private val imageLoader: Lazy, private val platformContext: Lazy, ) { - private val cache = lruCache(100) - @OptIn(ExperimentalCoilApi::class) - suspend fun calculateSeedColor(url: String): Color { - val cached = cache[url] - if (cached != null) { - return cached - } + suspend fun calculateSeedColor(url: String?): Int? { + if (url.isNullOrBlank()) return null val bitmap = suspendCancellableCoroutine { cont -> val request = @@ -61,14 +54,14 @@ class SeedColorExtractor( imageLoader.value.enqueue(request) } - return bitmap.seedColor().also { cache.put(url, it) } + return bitmap.seedColor() } - private fun ImageBitmap.seedColor(): Color { + private fun ImageBitmap.seedColor(): Int { val bitmapPixels = IntArray(width * height) readPixels(buffer = bitmapPixels) - return Color(Score.score(QuantizerCelebi.quantize(bitmapPixels, maxColors = 128)).first()) + return Score.score(QuantizerCelebi.quantize(bitmapPixels, maxColors = 128)).first() } private companion object { From dcdb259b1f51845cac35f26db146f827c9ba6b0a Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 19:55:30 +0530 Subject: [PATCH 05/21] Add default light color scheme --- .../sasikanth/rss/reader/ui/AppColorScheme.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt index 3eb4194c1..acf67c14c 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt @@ -129,6 +129,27 @@ class AppColorScheme( ) } +fun lightAppColorScheme(): AppColorScheme { + return AppColorScheme( + tintedBackground = Color(0xFFF4FFF8), + tintedSurface = Color(0xFFBAFFE4), + tintedForeground = Color(0xFF006C53), + tintedHighlight = Color(0xFF63DBB5), + outline = Color(0xFF6A7771), + outlineVariant = Color(0xFFBCCAC2), + surface = Color(0xFFF5FBF6), + onSurface = Color(0xFF171D1A), + onSurfaceVariant = Color(0xFF3D4944), + surfaceContainer = Color(0xFFE9EFEA), + surfaceContainerLow = Color(0xFFEFF5F0), + surfaceContainerLowest = Color(0xFFFFFFFF), + surfaceContainerHigh = Color(0xFFE4EAE5), + surfaceContainerHighest = Color(0xFFDEE4DF), + textEmphasisHigh = Color.Black.copy(alpha = 0.9f), + textEmphasisMed = Color.Black.copy(alpha = 0.7f) + ) +} + fun darkAppColorScheme(): AppColorScheme { return AppColorScheme( tintedBackground = Color(0xFF002117), From 55d882de9c4008a17242ceecf85120120bfb46b7 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 19:56:38 +0530 Subject: [PATCH 06/21] Use color scheme tokens in `PostMetadata` --- .../dev/sasikanth/rss/reader/home/ui/PostMetadata.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostMetadata.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostMetadata.kt index 435914b65..4f565af53 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostMetadata.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/PostMetadata.kt @@ -135,7 +135,7 @@ private fun PostSourcePill( if (config.enablePostSource) { Modifier.clip(RoundedCornerShape(50)) .clickable(onClick = onSourceClick) - .background(color = Color.White.copy(alpha = 0.12f)) + .background(color = AppTheme.colorScheme.textEmphasisHigh.copy(alpha = 0.12f)) .padding(vertical = 4.dp) .padding(start = 8.dp, end = 12.dp) } else { @@ -144,7 +144,7 @@ private fun PostSourcePill( val postSourceTextColor = if (config.enablePostSource) { - Color.White + AppTheme.colorScheme.textEmphasisHigh } else { AppTheme.colorScheme.onSurfaceVariant } @@ -187,7 +187,7 @@ private fun PostOptionsButtonRow( if (!commentsLink.isNullOrBlank()) { PostOptionIconButton( icon = TwineIcons.Comments, - iconTint = Color.White, + iconTint = AppTheme.colorScheme.textEmphasisHigh, contentDescription = LocalStrings.current.comments, onClick = onCommentsClick ) @@ -204,7 +204,7 @@ private fun PostOptionsButtonRow( if (postBookmarked) { AppTheme.colorScheme.tintedForeground } else { - Color.White + AppTheme.colorScheme.textEmphasisHigh }, contentDescription = LocalStrings.current.bookmark, onClick = onBookmarkClick @@ -320,7 +320,7 @@ private fun PostOptionIconButton( icon: ImageVector, contentDescription: String, modifier: Modifier = Modifier, - iconTint: Color = Color.White, + iconTint: Color = AppTheme.colorScheme.textEmphasisHigh, onClick: () -> Unit, ) { Box( From 71198db1325d31d3d02a82462c6c5652f368b4eb Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 19:57:03 +0530 Subject: [PATCH 07/21] Hide `FeaturedSection` if there are no featured posts --- .../sasikanth/rss/reader/home/ui/PostList.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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 c6b867b14..a5fae216c 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 @@ -92,18 +92,21 @@ internal fun PostsList( contentPadding = PaddingValues(top = topContentPadding, bottom = BOTTOM_SHEET_PEEK_HEIGHT + 120.dp) ) { - item { - FeaturedSection( - paddingValues = paddingValues, - pagerState = featuredPostsPagerState, - featuredPosts = featuredPosts, - featuredItemBlurEnabled = featuredItemBlurEnabled, - onItemClick = onPostClicked, - onPostBookmarkClick = onPostBookmarkClick, - onPostCommentsClick = onPostCommentsClick, - onPostSourceClick = onPostSourceClick, - onTogglePostReadClick = onTogglePostReadClick - ) + if (featuredPosts.isNotEmpty()) { + item { + FeaturedSection( + paddingValues = paddingValues, + pagerState = featuredPostsPagerState, + featuredPosts = featuredPosts, + featuredItemBlurEnabled = featuredItemBlurEnabled, + useDarkTheme = useDarkTheme, + onItemClick = onPostClicked, + onPostBookmarkClick = onPostBookmarkClick, + onPostCommentsClick = onPostCommentsClick, + onPostSourceClick = onPostSourceClick, + onTogglePostReadClick = onTogglePostReadClick + ) + } } items(posts.itemCount) { index -> From 086699990a91039d87683b365c067369dd8a05b5 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Tue, 6 Aug 2024 19:58:11 +0530 Subject: [PATCH 08/21] Add support for dynamic light theme --- .../dev/sasikanth/rss/reader/MainActivity.kt | 19 +- .../dev/sasikanth/rss/reader/app/App.kt | 68 +- .../sasikanth/rss/reader/app/AppPresenter.kt | 28 +- .../dev/sasikanth/rss/reader/app/AppState.kt | 26 + .../reader/components/DynamicContentTheme.kt | 600 ------------------ .../rss/reader/feed/ui/FeedInfoBottomSheet.kt | 117 ++-- .../groupselection/ui/GroupSelectionSheet.kt | 179 +++--- .../rss/reader/home/HomePresenter.kt | 28 +- .../sasikanth/rss/reader/home/HomeState.kt | 3 +- .../rss/reader/home/ui/FeaturedPostItem.kt | 3 + .../rss/reader/home/ui/FeaturedSection.kt | 172 +++-- .../rss/reader/home/ui/HomeScreen.kt | 252 ++++---- .../sasikanth/rss/reader/home/ui/PostList.kt | 3 +- .../dev/sasikanth/rss/reader/ui/AppTheme.kt | 6 +- .../rss/reader/ui/DynamicContentTheme.kt | 350 ++++++++++ .../rss/reader/HomeViewController.kt | 9 +- 16 files changed, 876 insertions(+), 987 deletions(-) create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppState.kt delete mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/DynamicContentTheme.kt create mode 100644 shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt diff --git a/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/MainActivity.kt b/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/MainActivity.kt index 889f11f6e..0168dd551 100644 --- a/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/MainActivity.kt +++ b/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/MainActivity.kt @@ -40,14 +40,23 @@ class MainActivity : ComponentActivity() { FileKit.init(this) - enableEdgeToEdge( - statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT), - navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT) - ) + enableEdgeToEdge() val activityComponent = ActivityComponent::class.create(activity = this) - setContent { activityComponent.app() } + setContent { + activityComponent.app { useDarkTheme -> + enableEdgeToEdge( + statusBarStyle = + if (useDarkTheme) { + SystemBarStyle.dark(Color.TRANSPARENT) + } else { + SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT) + }, + navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT) + ) + } + } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt index 4e59cd126..4f9756acd 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt @@ -15,13 +15,18 @@ */ package dev.sasikanth.rss.reader.app +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.SheetValue import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import coil3.ImageLoader import coil3.annotation.ExperimentalCoilApi @@ -33,9 +38,7 @@ import com.arkivanov.essenty.backhandler.BackHandler import dev.sasikanth.rss.reader.about.ui.AboutScreen import dev.sasikanth.rss.reader.addfeed.ui.AddFeedScreen import dev.sasikanth.rss.reader.bookmarks.ui.BookmarksScreen -import dev.sasikanth.rss.reader.components.DynamicContentTheme -import dev.sasikanth.rss.reader.components.LocalDynamicColorState -import dev.sasikanth.rss.reader.components.rememberDynamicColorState +import dev.sasikanth.rss.reader.data.repository.AppThemeMode import dev.sasikanth.rss.reader.feed.ui.FeedInfoBottomSheet import dev.sasikanth.rss.reader.group.ui.GroupScreen import dev.sasikanth.rss.reader.groupselection.ui.GroupSelectionSheet @@ -49,11 +52,16 @@ import dev.sasikanth.rss.reader.search.ui.SearchScreen import dev.sasikanth.rss.reader.settings.ui.SettingsScreen import dev.sasikanth.rss.reader.share.LocalShareHandler import dev.sasikanth.rss.reader.share.ShareHandler +import dev.sasikanth.rss.reader.ui.DynamicContentTheme +import dev.sasikanth.rss.reader.ui.darkAppColorScheme +import dev.sasikanth.rss.reader.ui.lightAppColorScheme +import dev.sasikanth.rss.reader.ui.rememberDynamicColorState import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.LocalWindowSizeClass +import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject -typealias App = @Composable () -> Unit +typealias App = @Composable (onThemeChange: (useDarkTheme: Boolean) -> Unit) -> Unit @Inject @Composable @@ -64,18 +72,36 @@ fun App( linkHandler: LinkHandler, imageLoader: ImageLoader, dispatchersProvider: DispatchersProvider, + @Assisted onThemeChange: (useDarkTheme: Boolean) -> Unit ) { - setSingletonImageLoaderFactory { imageLoader } + val appState by appPresenter.state.collectAsState() - val dynamicColorState = rememberDynamicColorState(imageLoader = imageLoader) + setSingletonImageLoaderFactory { imageLoader } CompositionLocalProvider( LocalWindowSizeClass provides calculateWindowSizeClass(), - LocalDynamicColorState provides dynamicColorState, LocalShareHandler provides shareHandler, - LocalLinkHandler provides linkHandler + LocalLinkHandler provides linkHandler, ) { - DynamicContentTheme(dynamicColorState) { + val isSystemInDarkTheme = isSystemInDarkTheme() + val useDarkTheme = + remember(isSystemInDarkTheme) { + when (appState.appThemeMode) { + AppThemeMode.Light -> false + AppThemeMode.Dark -> true + AppThemeMode.Auto -> isSystemInDarkTheme + } + } + + LaunchedEffect(useDarkTheme) { onThemeChange(useDarkTheme) } + + val dynamicColorsState = + rememberDynamicColorState( + defaultLightAppColorScheme = lightAppColorScheme(), + defaultDarkAppColorScheme = darkAppColorScheme(), + ) + + DynamicContentTheme(state = dynamicColorsState, useDarkTheme = useDarkTheme) { ProvideStrings { Box { Children( @@ -93,7 +119,21 @@ fun App( PlaceholderScreen(modifier = fillMaxSizeModifier) } is Screen.Home -> { - HomeScreen(homePresenter = screen.presenter, modifier = fillMaxSizeModifier) + HomeScreen( + homePresenter = screen.presenter, + useDarkTheme = useDarkTheme, + modifier = fillMaxSizeModifier, + onBottomSheetStateChanged = { sheetValue -> + val tempUseDarkTheme = + if (sheetValue == SheetValue.Expanded) { + true + } else { + useDarkTheme + } + + onThemeChange(tempUseDarkTheme) + } + ) } is Screen.Search -> { SearchScreen(searchPresenter = screen.presenter, modifier = fillMaxSizeModifier) @@ -118,10 +158,14 @@ fun App( ) } is Screen.AddFeed -> { - AddFeedScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) + DynamicContentTheme(useDarkTheme = true) { + AddFeedScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) + } } is Screen.GroupDetails -> { - GroupScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) + DynamicContentTheme(useDarkTheme = true) { + GroupScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) + } } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt index a4f455fd2..ec0357e30 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt @@ -56,8 +56,15 @@ import dev.sasikanth.rss.reader.util.DispatchersProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @@ -89,7 +96,8 @@ class AppPresenter( PresenterInstance( dispatchersProvider = dispatchersProvider, lastUpdatedAt = lastUpdatedAt, - rssRepository = rssRepository + rssRepository = rssRepository, + settingsRepository = settingsRepository, ) } @@ -113,6 +121,8 @@ class AppPresenter( childFactory = ::createModal, ) + internal val state = presenterInstance.state + private val scope = coroutineScope(dispatchersProvider.main + SupervisorJob()) init { @@ -262,10 +272,24 @@ class AppPresenter( private class PresenterInstance( dispatchersProvider: DispatchersProvider, private val lastUpdatedAt: LastUpdatedAt, - private val rssRepository: RssRepository + private val rssRepository: RssRepository, + private val settingsRepository: SettingsRepository, ) : InstanceKeeper.Instance { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val _state = MutableStateFlow(AppState.DEFAULT) + val state: StateFlow = + _state.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = AppState.DEFAULT + ) + + init { + settingsRepository.appThemeMode + .onEach { appThemeMode -> _state.update { it.copy(appThemeMode = appThemeMode) } } + .launchIn(coroutineScope) + } fun refreshFeedsIfExpired() { coroutineScope.launch { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppState.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppState.kt new file mode 100644 index 000000000..83795635b --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppState.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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.app + +import dev.sasikanth.rss.reader.data.repository.AppThemeMode + +data class AppState(val appThemeMode: AppThemeMode) { + + companion object { + val DEFAULT = AppState(appThemeMode = AppThemeMode.Auto) + } +} diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/DynamicContentTheme.kt deleted file mode 100644 index c71a17c20..000000000 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/components/DynamicContentTheme.kt +++ /dev/null @@ -1,600 +0,0 @@ -/* - * Copyright 2023 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.components - -import androidx.collection.LruCache -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.mapSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.lerp -import coil3.ImageLoader -import coil3.PlatformContext -import coil3.annotation.ExperimentalCoilApi -import coil3.compose.LocalPlatformContext -import coil3.request.ImageRequest -import coil3.size.Scale -import dev.sasikanth.material.color.utilities.dynamiccolor.DynamicColor -import dev.sasikanth.material.color.utilities.dynamiccolor.MaterialDynamicColors -import dev.sasikanth.material.color.utilities.dynamiccolor.ToneDeltaConstraint -import dev.sasikanth.material.color.utilities.dynamiccolor.TonePolarity -import dev.sasikanth.material.color.utilities.hct.Hct -import dev.sasikanth.material.color.utilities.quantize.QuantizerCelebi -import dev.sasikanth.material.color.utilities.scheme.DynamicScheme -import dev.sasikanth.material.color.utilities.scheme.SchemeContent -import dev.sasikanth.material.color.utilities.score.Score -import dev.sasikanth.rss.reader.ui.AppTheme -import dev.sasikanth.rss.reader.utils.Constants.EPSILON -import dev.sasikanth.rss.reader.utils.inverse -import dev.sasikanth.rss.reader.utils.toComposeImageBitmap -import kotlin.math.absoluteValue -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.withContext - -private const val TINTED_BACKGROUND = "tinted_background" -private const val TINTED_SURFACE = "tinted_surface" -private const val TINTED_FOREGROUND = "tinted_foreground" -private const val TINTED_HIGHLIGHT = "tinted_highlight" -private const val OUTLINE = "outline" -private const val OUTLINE_VARIANT = "outline_VARIANT" -private const val SURFACE = "surface" -private const val ON_SURFACE = "on_surface" -private const val ON_SURFACE_VARIANT = "on_surface_variant" -private const val SURFACE_CONTAINER = "surface_container" -private const val SURFACE_CONTAINER_LOW = "surface_container_low" -private const val SURFACE_CONTAINER_LOWEST = "surface_container_lowest" -private const val SURFACE_CONTAINER_HIGH = "surface_container_high" -private const val SURFACE_CONTAINER_HIGHEST = "surface_container_highest" - -@Composable -internal fun DynamicContentTheme( - dynamicColorState: DynamicColorState, - content: @Composable () -> Unit -) { - val colorScheme = - AppTheme.colorScheme.copy( - tintedBackground = dynamicColorState.tintedBackground, - tintedSurface = dynamicColorState.tintedSurface, - tintedForeground = dynamicColorState.tintedForeground, - tintedHighlight = dynamicColorState.tintedHighlight, - outline = dynamicColorState.outline, - outlineVariant = dynamicColorState.outlineVariant, - surface = dynamicColorState.surface, - onSurface = dynamicColorState.onSurface, - onSurfaceVariant = dynamicColorState.onSurfaceVariant, - surfaceContainer = dynamicColorState.surfaceContainer, - surfaceContainerLow = dynamicColorState.surfaceContainerLow, - surfaceContainerLowest = dynamicColorState.surfaceContainerLowest, - surfaceContainerHigh = dynamicColorState.surfaceContainerHigh, - surfaceContainerHighest = dynamicColorState.surfaceContainerHighest, - ) - - AppTheme(appColorScheme = colorScheme, content = content) -} - -@Composable -internal fun rememberDynamicColorState( - defaultTintedBackground: Color = AppTheme.colorScheme.tintedBackground, - defaultTintedSurface: Color = AppTheme.colorScheme.tintedSurface, - defaultTintedForeground: Color = AppTheme.colorScheme.tintedForeground, - defaultTintedHighlight: Color = AppTheme.colorScheme.tintedHighlight, - defaultOutline: Color = AppTheme.colorScheme.outline, - defaultOutlineVariant: Color = AppTheme.colorScheme.outlineVariant, - defaultSurface: Color = AppTheme.colorScheme.surface, - defaultOnSurface: Color = AppTheme.colorScheme.onSurface, - defaultOnSurfaceVariant: Color = AppTheme.colorScheme.onSurfaceVariant, - defaultSurfaceContainer: Color = AppTheme.colorScheme.surfaceContainer, - defaultSurfaceContainerLow: Color = AppTheme.colorScheme.surfaceContainerLow, - defaultSurfaceContainerLowest: Color = AppTheme.colorScheme.surfaceContainerLowest, - defaultSurfaceContainerHigh: Color = AppTheme.colorScheme.surfaceContainerHigh, - defaultSurfaceContainerHighest: Color = AppTheme.colorScheme.surfaceContainerHighest, - imageLoader: ImageLoader, -): DynamicColorState { - val platformContext = LocalPlatformContext.current - return rememberSaveable(saver = DynamicColorState.Saver) { - DynamicColorState( - defaultTintedBackground, - defaultTintedSurface, - defaultTintedForeground, - defaultTintedHighlight, - defaultOutline, - defaultOutlineVariant, - defaultSurface, - defaultOnSurface, - defaultOnSurfaceVariant, - defaultSurfaceContainer, - defaultSurfaceContainerLow, - defaultSurfaceContainerLowest, - defaultSurfaceContainerHigh, - defaultSurfaceContainerHighest, - ) - } - .apply { setImageLoader(imageLoader, platformContext) } -} - -/** - * A class which stores and caches the result of any calculated colors from images. - * - * @param cacheSize The size of the [LruCache] used to store recent results. Pass `0` to disable the - * cache. - */ -@Stable -internal class DynamicColorState( - private val defaultTintedBackground: Color, - private val defaultTintedSurface: Color, - private val defaultTintedForeground: Color, - private val defaultTintedHighlight: Color, - private val defaultOutline: Color, - private val defaultOutlineVariant: Color, - private val defaultSurface: Color, - private val defaultOnSurface: Color, - private val defaultOnSurfaceVariant: Color, - private val defaultSurfaceContainer: Color, - private val defaultSurfaceContainerLow: Color, - private val defaultSurfaceContainerLowest: Color, - private val defaultSurfaceContainerHigh: Color, - private val defaultSurfaceContainerHighest: Color, - cacheSize: Int = 15 -) { - var tintedBackground by mutableStateOf(defaultTintedBackground) - private set - - var tintedSurface by mutableStateOf(defaultTintedSurface) - private set - - var tintedForeground by mutableStateOf(defaultTintedForeground) - private set - - var tintedHighlight by mutableStateOf(defaultTintedHighlight) - private set - - var outline by mutableStateOf(defaultOutline) - private set - - var outlineVariant by mutableStateOf(defaultOutlineVariant) - private set - - var surface by mutableStateOf(defaultSurfaceContainer) - private set - - var onSurface by mutableStateOf(defaultOnSurface) - private set - - var onSurfaceVariant by mutableStateOf(defaultOnSurfaceVariant) - private set - - var surfaceContainer by mutableStateOf(defaultSurfaceContainer) - private set - - var surfaceContainerLow by mutableStateOf(defaultSurfaceContainerLow) - private set - - var surfaceContainerLowest by mutableStateOf(defaultSurfaceContainerLowest) - private set - - var surfaceContainerHigh by mutableStateOf(defaultSurfaceContainerHigh) - private set - - var surfaceContainerHighest by mutableStateOf(defaultSurfaceContainerHighest) - private set - - private val cache = - when { - cacheSize > 0 -> LruCache(cacheSize) - else -> null - } - private var images = emptyList() - - private lateinit var imageLoader: ImageLoader - private lateinit var platformContext: PlatformContext - - companion object { - val Saver: Saver = - mapSaver( - save = { - mapOf( - TINTED_BACKGROUND to it.tintedBackground.value.toString(), - TINTED_SURFACE to it.tintedSurface.value.toString(), - TINTED_FOREGROUND to it.tintedForeground.value.toString(), - TINTED_HIGHLIGHT to it.tintedHighlight.value.toString(), - OUTLINE to it.outline.value.toString(), - OUTLINE_VARIANT to it.outlineVariant.value.toString(), - SURFACE to it.surface.value.toString(), - ON_SURFACE to it.onSurface.value.toString(), - ON_SURFACE_VARIANT to it.onSurfaceVariant.value.toString(), - SURFACE_CONTAINER to it.surfaceContainer.value.toString(), - SURFACE_CONTAINER_LOW to it.surfaceContainerLow.value.toString(), - SURFACE_CONTAINER_LOWEST to it.surfaceContainerLowest.value.toString(), - SURFACE_CONTAINER_HIGH to it.surfaceContainerHigh.value.toString(), - SURFACE_CONTAINER_HIGHEST to it.surfaceContainerHighest.value.toString(), - ) - }, - restore = { - DynamicColorState( - defaultTintedBackground = Color(it[TINTED_BACKGROUND].toString().toULong()), - defaultTintedSurface = Color(it[TINTED_SURFACE].toString().toULong()), - defaultTintedForeground = Color(it[TINTED_FOREGROUND].toString().toULong()), - defaultTintedHighlight = Color(it[TINTED_HIGHLIGHT].toString().toULong()), - defaultOutline = Color(it[OUTLINE].toString().toULong()), - defaultOutlineVariant = Color(it[OUTLINE_VARIANT].toString().toULong()), - defaultSurface = Color(it[SURFACE].toString().toULong()), - defaultOnSurface = Color(it[ON_SURFACE].toString().toULong()), - defaultOnSurfaceVariant = Color(it[ON_SURFACE_VARIANT].toString().toULong()), - defaultSurfaceContainer = Color(it[SURFACE_CONTAINER].toString().toULong()), - defaultSurfaceContainerLow = Color(it[SURFACE_CONTAINER_LOW].toString().toULong()), - defaultSurfaceContainerLowest = - Color(it[SURFACE_CONTAINER_LOWEST].toString().toULong()), - defaultSurfaceContainerHigh = Color(it[SURFACE_CONTAINER_HIGH].toString().toULong()), - defaultSurfaceContainerHighest = - Color(it[SURFACE_CONTAINER_HIGHEST].toString().toULong()), - ) - } - ) - } - - fun setImageLoader(imageLoader: ImageLoader, platformContext: PlatformContext) { - this.imageLoader = imageLoader - this.platformContext = platformContext - } - - suspend fun onContentChange(newImages: List) { - if (!images.containsAll(newImages)) { - images = newImages - } - images.forEach { imageUrl -> fetchDynamicColors(imageUrl) } - } - - fun updateOffset( - previousImageUrl: String?, - currentImageUrl: String, - nextImageUrl: String?, - offset: Float - ) { - val previousDynamicColors = previousImageUrl?.let { cache?.get(it) } - val currentDynamicColors = cache?.get(currentImageUrl) - val nextDynamicColors = nextImageUrl?.let { cache?.get(it) } - - tintedBackground = - interpolateColors( - previous = previousDynamicColors?.tintedBackground, - current = currentDynamicColors?.tintedBackground, - next = nextDynamicColors?.tintedBackground, - default = defaultTintedBackground, - fraction = offset - ) - tintedSurface = - interpolateColors( - previous = previousDynamicColors?.tintedSurface, - current = currentDynamicColors?.tintedSurface, - next = nextDynamicColors?.tintedSurface, - default = defaultTintedSurface, - fraction = offset - ) - tintedForeground = - interpolateColors( - previous = previousDynamicColors?.tintedForeground, - current = currentDynamicColors?.tintedForeground, - next = nextDynamicColors?.tintedForeground, - default = defaultTintedForeground, - fraction = offset - ) - tintedHighlight = - interpolateColors( - previous = previousDynamicColors?.tintedHighlight, - current = currentDynamicColors?.tintedHighlight, - next = nextDynamicColors?.tintedHighlight, - default = defaultTintedHighlight, - fraction = offset - ) - outline = - interpolateColors( - previous = previousDynamicColors?.outline, - current = currentDynamicColors?.outline, - next = nextDynamicColors?.outline, - default = defaultOutline, - fraction = offset - ) - outlineVariant = - interpolateColors( - previous = previousDynamicColors?.outlineVariant, - current = currentDynamicColors?.outlineVariant, - next = nextDynamicColors?.outlineVariant, - default = defaultOutlineVariant, - fraction = offset - ) - surface = - interpolateColors( - previous = previousDynamicColors?.surface, - current = currentDynamicColors?.surface, - next = nextDynamicColors?.surface, - default = defaultSurface, - fraction = offset - ) - onSurface = - interpolateColors( - previous = previousDynamicColors?.onSurface, - current = currentDynamicColors?.onSurface, - next = nextDynamicColors?.onSurface, - default = defaultOnSurface, - fraction = offset - ) - onSurfaceVariant = - interpolateColors( - previous = previousDynamicColors?.onSurfaceVariant, - current = currentDynamicColors?.onSurfaceVariant, - next = nextDynamicColors?.onSurfaceVariant, - default = defaultOnSurfaceVariant, - fraction = offset - ) - surfaceContainer = - interpolateColors( - previous = previousDynamicColors?.surfaceContainer, - current = currentDynamicColors?.surfaceContainer, - next = nextDynamicColors?.surfaceContainer, - default = defaultSurfaceContainer, - fraction = offset - ) - surfaceContainerLow = - interpolateColors( - previous = previousDynamicColors?.surfaceContainerLow, - current = currentDynamicColors?.surfaceContainerLow, - next = nextDynamicColors?.surfaceContainerLow, - default = defaultSurfaceContainerLow, - fraction = offset - ) - surfaceContainerLowest = - interpolateColors( - previous = previousDynamicColors?.surfaceContainerLowest, - current = currentDynamicColors?.surfaceContainerLowest, - next = nextDynamicColors?.surfaceContainerLowest, - default = defaultSurfaceContainerLowest, - fraction = offset - ) - surfaceContainerHigh = - interpolateColors( - previous = previousDynamicColors?.surfaceContainerHigh, - current = currentDynamicColors?.surfaceContainerHigh, - next = nextDynamicColors?.surfaceContainerHigh, - default = defaultSurfaceContainerHigh, - fraction = offset - ) - surfaceContainerHighest = - interpolateColors( - previous = previousDynamicColors?.surfaceContainerHighest, - current = currentDynamicColors?.surfaceContainerHighest, - next = nextDynamicColors?.surfaceContainerHighest, - default = defaultSurfaceContainerHighest, - fraction = offset - ) - } - - fun reset() { - tintedBackground = defaultTintedBackground - tintedSurface = defaultTintedSurface - tintedForeground = defaultTintedForeground - tintedHighlight = defaultTintedHighlight - outline = defaultOutline - outlineVariant = defaultOutlineVariant - surface = defaultSurface - onSurface = defaultOnSurface - onSurfaceVariant = defaultOnSurfaceVariant - surfaceContainer = defaultSurfaceContainer - surfaceContainerLow = defaultSurfaceContainerLow - surfaceContainerLowest = defaultSurfaceContainerLowest - surfaceContainerHigh = defaultSurfaceContainerHigh - surfaceContainerHighest = defaultSurfaceContainerHighest - } - - @OptIn(ExperimentalCoilApi::class) - private suspend fun fetchDynamicColors(url: String): DynamicColors? { - val cached = cache?.get(url) - if (cached != null) { - // If we already have the result cached, return early now... - return cached - } - - val imageRequest = - ImageRequest.Builder(platformContext) - .data(url) - .scale(Scale.FILL) - .size(64) - .memoryCacheKey("$url.dynamic_colors") - .build() - - val image = - withContext(Dispatchers.IO) { - imageLoader.execute(imageRequest).image?.toComposeImageBitmap(platformContext) - } - - return if (image != null) { - extractColorsFromImage(image) - .let { colorsMap -> - return@let if (colorsMap.isNotEmpty()) { - DynamicColors( - tintedBackground = Color(colorsMap[TINTED_BACKGROUND]!!), - tintedSurface = Color(colorsMap[TINTED_SURFACE]!!), - tintedForeground = Color(colorsMap[TINTED_FOREGROUND]!!), - tintedHighlight = Color(colorsMap[TINTED_HIGHLIGHT]!!), - outline = Color(colorsMap[OUTLINE]!!), - outlineVariant = Color(colorsMap[OUTLINE_VARIANT]!!), - surface = Color(colorsMap[SURFACE]!!), - onSurface = Color(colorsMap[ON_SURFACE]!!), - onSurfaceVariant = Color(colorsMap[ON_SURFACE_VARIANT]!!), - surfaceContainer = Color(colorsMap[SURFACE_CONTAINER]!!), - surfaceContainerLow = Color(colorsMap[SURFACE_CONTAINER_LOW]!!), - surfaceContainerLowest = Color(colorsMap[SURFACE_CONTAINER_LOWEST]!!), - surfaceContainerHigh = Color(colorsMap[SURFACE_CONTAINER_HIGH]!!), - surfaceContainerHighest = Color(colorsMap[SURFACE_CONTAINER_HIGHEST]!!) - ) - } else { - null - } - } - ?.also { result -> cache?.put(url, result) } - } else null - } - - private suspend fun extractColorsFromImage(image: ImageBitmap): Map { - val colorMap: MutableMap = mutableMapOf() - withContext(Dispatchers.Default) { - val bitmapPixels = IntArray(image.width * image.height) - image.readPixels(buffer = bitmapPixels) - - val seedColor = Score.score(QuantizerCelebi.quantize(bitmapPixels, 128)).first() - val scheme = - SchemeContent(sourceColorHct = Hct.fromInt(seedColor), isDark = true, contrastLevel = 0.0) - - val dynamicColors = MaterialDynamicColors() - val tokens = - mapOf( - TINTED_BACKGROUND to - DynamicColor.fromPalette( - palette = { s: DynamicScheme -> s.primaryPalette }, - tone = { _ -> 10.0 }, - background = { s: DynamicScheme -> dynamicColors.highestSurface(s) }, - toneDeltaConstraint = { _ -> - ToneDeltaConstraint( - MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, - dynamicColors.primaryContainer(), - TonePolarity.DARKER - ) - } - ), - TINTED_SURFACE to - DynamicColor.fromPalette( - palette = { s: DynamicScheme -> s.primaryPalette }, - tone = { _ -> 20.0 }, - background = { s: DynamicScheme -> dynamicColors.highestSurface(s) }, - toneDeltaConstraint = { _ -> - ToneDeltaConstraint( - MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, - dynamicColors.primaryContainer(), - TonePolarity.DARKER - ) - } - ), - TINTED_FOREGROUND to - DynamicColor.fromPalette( - palette = { s: DynamicScheme -> s.primaryPalette }, - tone = { _ -> 80.0 }, - background = { s: DynamicScheme -> dynamicColors.highestSurface(s) }, - toneDeltaConstraint = { _ -> - ToneDeltaConstraint( - MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, - dynamicColors.primaryContainer(), - TonePolarity.DARKER - ) - } - ), - TINTED_HIGHLIGHT to - DynamicColor.fromPalette( - palette = { s: DynamicScheme -> s.primaryPalette }, - tone = { _: DynamicScheme -> 40.0 }, - background = { s: DynamicScheme -> dynamicColors.highestSurface(s) }, - toneDeltaConstraint = { _: DynamicScheme -> - ToneDeltaConstraint( - MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, - dynamicColors.primaryContainer(), - TonePolarity.DARKER - ) - } - ), - OUTLINE to dynamicColors.outline(), - OUTLINE_VARIANT to dynamicColors.outlineVariant(), - SURFACE to dynamicColors.surface(), - ON_SURFACE to dynamicColors.onSurface(), - ON_SURFACE_VARIANT to dynamicColors.onSurfaceVariant(), - SURFACE_CONTAINER to dynamicColors.surfaceContainer(), - SURFACE_CONTAINER_LOW to dynamicColors.surfaceContainerLow(), - SURFACE_CONTAINER_LOWEST to dynamicColors.surfaceContainerLowest(), - SURFACE_CONTAINER_HIGH to dynamicColors.surfaceContainerHigh(), - SURFACE_CONTAINER_HIGHEST to dynamicColors.surfaceContainerHighest(), - ) - - for (token in tokens) { - colorMap[token.key] = token.value.getArgb(scheme) - } - } - return colorMap - } - - private fun interpolateColors( - previous: Color?, - current: Color?, - next: Color?, - default: Color, - fraction: Float - ): Color { - val startColor = - if (fraction < -EPSILON) { - previous - } else { - current - } - - val endColor = - if (fraction > EPSILON) { - next - } else { - current - } - - if (startColor == null || endColor == null) { - return default - } - - val normalizedOffset = - if (fraction < -EPSILON) { - fraction.absoluteValue.inverse() - } else { - fraction - } - - return lerp(startColor, endColor, normalizedOffset) - } -} - -@Immutable -private data class DynamicColors( - val tintedBackground: Color, - val tintedSurface: Color, - val tintedForeground: Color, - val tintedHighlight: Color, - val outline: Color, - val outlineVariant: Color, - val surface: Color, - val onSurface: Color, - val onSurfaceVariant: Color, - val surfaceContainer: Color, - val surfaceContainerLow: Color, - val surfaceContainerLowest: Color, - val surfaceContainerHigh: Color, - val surfaceContainerHighest: Color, -) - -internal val LocalDynamicColorState = - staticCompositionLocalOf { - throw NullPointerException("Please provide a dynamic color state") - } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt index ed32ad3d8..b30560533 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feed/ui/FeedInfoBottomSheet.kt @@ -91,6 +91,7 @@ import dev.sasikanth.rss.reader.resources.icons.Website import dev.sasikanth.rss.reader.resources.strings.LocalStrings import dev.sasikanth.rss.reader.share.LocalShareHandler import dev.sasikanth.rss.reader.ui.AppTheme +import dev.sasikanth.rss.reader.ui.DynamicContentTheme import dev.sasikanth.rss.reader.ui.SYSTEM_SCRIM import dev.sasikanth.rss.reader.utils.KeyboardState import dev.sasikanth.rss.reader.utils.keyboardVisibilityAsState @@ -116,64 +117,66 @@ fun FeedInfoBottomSheet( } } - ModalBottomSheet( - modifier = Modifier.then(modifier), - onDismissRequest = { feedPresenter.dispatch(FeedEvent.BackClicked) }, - containerColor = AppTheme.colorScheme.tintedBackground, - contentColor = Color.Unspecified, - windowInsets = - WindowInsets.systemBars - .only(WindowInsetsSides.Bottom) - .union(WindowInsets.ime.only(WindowInsetsSides.Bottom)), - sheetState = SheetState(skipPartiallyExpanded = true, density = LocalDensity.current), - scrimColor = SYSTEM_SCRIM - ) { - Column( - modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + DynamicContentTheme(useDarkTheme = true) { + ModalBottomSheet( + modifier = Modifier.then(modifier), + onDismissRequest = { feedPresenter.dispatch(FeedEvent.BackClicked) }, + containerColor = AppTheme.colorScheme.tintedBackground, + contentColor = Color.Unspecified, + windowInsets = + WindowInsets.systemBars + .only(WindowInsetsSides.Bottom) + .union(WindowInsets.ime.only(WindowInsetsSides.Bottom)), + sheetState = SheetState(skipPartiallyExpanded = true, density = LocalDensity.current), + scrimColor = SYSTEM_SCRIM ) { - val feed = state.feed - if (feed != null) { - FeedLabelInput( - modifier = Modifier.padding(horizontal = HORIZONTAL_PADDING), - feed = feed, - onFeedNameChange = { newFeedName -> - feedPresenter.dispatch( - FeedEvent.OnFeedNameChanged(newFeedName = newFeedName, feedId = feed.id) - ) - } - ) - - Spacer(Modifier.requiredHeight(8.dp)) - - FeedUnreadCount( - modifier = Modifier.fillMaxWidth().padding(horizontal = HORIZONTAL_PADDING), - numberOfUnreadPosts = feed.numberOfUnreadPosts, - onMarkPostsAsRead = { feedPresenter.dispatch(FeedEvent.OnMarkPostsAsRead(feed.id)) } - ) - - Divider(horizontalInsets = HORIZONTAL_PADDING) - - AlwaysFetchSourceArticleSwitch( - feed = feed, - onValueChanged = { newValue, feedId -> - feedPresenter.dispatch(FeedEvent.OnAlwaysFetchSourceArticleChanged(newValue, feedId)) - } - ) - - Divider(horizontalInsets = HORIZONTAL_PADDING) - - FeedOptions( - modifier = Modifier.padding(horizontal = HORIZONTAL_PADDING), - feed = feed, - onRemoveFeedClick = { feedPresenter.dispatch(FeedEvent.RemoveFeedClicked) } - ) - - Spacer(Modifier.requiredHeight(8.dp)) - } else { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.CenterHorizontally), - color = AppTheme.colorScheme.tintedForeground - ) + Column( + modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + ) { + val feed = state.feed + if (feed != null) { + FeedLabelInput( + modifier = Modifier.padding(horizontal = HORIZONTAL_PADDING), + feed = feed, + onFeedNameChange = { newFeedName -> + feedPresenter.dispatch( + FeedEvent.OnFeedNameChanged(newFeedName = newFeedName, feedId = feed.id) + ) + } + ) + + Spacer(Modifier.requiredHeight(8.dp)) + + FeedUnreadCount( + modifier = Modifier.fillMaxWidth().padding(horizontal = HORIZONTAL_PADDING), + numberOfUnreadPosts = feed.numberOfUnreadPosts, + onMarkPostsAsRead = { feedPresenter.dispatch(FeedEvent.OnMarkPostsAsRead(feed.id)) } + ) + + Divider(horizontalInsets = HORIZONTAL_PADDING) + + AlwaysFetchSourceArticleSwitch( + feed = feed, + onValueChanged = { newValue, feedId -> + feedPresenter.dispatch(FeedEvent.OnAlwaysFetchSourceArticleChanged(newValue, feedId)) + } + ) + + Divider(horizontalInsets = HORIZONTAL_PADDING) + + FeedOptions( + modifier = Modifier.padding(horizontal = HORIZONTAL_PADDING), + feed = feed, + onRemoveFeedClick = { feedPresenter.dispatch(FeedEvent.RemoveFeedClicked) } + ) + + Spacer(Modifier.requiredHeight(8.dp)) + } else { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.CenterHorizontally), + color = AppTheme.colorScheme.tintedForeground + ) + } } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt index 47bd94d8a..16a035900 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt @@ -65,112 +65,115 @@ import dev.sasikanth.rss.reader.resources.icons.Add 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.ui.DynamicContentTheme import dev.sasikanth.rss.reader.ui.SYSTEM_SCRIM @Composable fun GroupSelectionSheet(presenter: GroupSelectionPresenter, modifier: Modifier = Modifier) { - ModalBottomSheet( - modifier = Modifier.then(modifier), - onDismissRequest = { presenter.dispatch(GroupSelectionEvent.BackClicked) }, - containerColor = AppTheme.colorScheme.tintedBackground, - contentColor = Color.Unspecified, - windowInsets = - WindowInsets.systemBars - .only(WindowInsetsSides.Bottom) - .union(WindowInsets.ime.only(WindowInsetsSides.Bottom)), - sheetState = SheetState(skipPartiallyExpanded = true, density = LocalDensity.current), - scrimColor = SYSTEM_SCRIM - ) { - val state by presenter.state.collectAsState() - val groups = state.groups.collectAsLazyPagingItems() + DynamicContentTheme(useDarkTheme = true) { + ModalBottomSheet( + modifier = Modifier.then(modifier), + onDismissRequest = { presenter.dispatch(GroupSelectionEvent.BackClicked) }, + containerColor = AppTheme.colorScheme.tintedBackground, + contentColor = Color.Unspecified, + windowInsets = + WindowInsets.systemBars + .only(WindowInsetsSides.Bottom) + .union(WindowInsets.ime.only(WindowInsetsSides.Bottom)), + sheetState = SheetState(skipPartiallyExpanded = true, density = LocalDensity.current), + scrimColor = SYSTEM_SCRIM + ) { + val state by presenter.state.collectAsState() + val groups = state.groups.collectAsLazyPagingItems() - var showCreateGroupDialog by remember { mutableStateOf(false) } + var showCreateGroupDialog by remember { mutableStateOf(false) } - if (showCreateGroupDialog) { - CreateGroupDialog( - onCreateGroup = { presenter.dispatch(GroupSelectionEvent.OnCreateGroup(it)) }, - onDismiss = { showCreateGroupDialog = false } - ) - } + if (showCreateGroupDialog) { + CreateGroupDialog( + onCreateGroup = { presenter.dispatch(GroupSelectionEvent.OnCreateGroup(it)) }, + onDismiss = { showCreateGroupDialog = false } + ) + } - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - item { - Box( - modifier = - Modifier.clip(MaterialTheme.shapes.large) - .background(AppTheme.colorScheme.tintedSurface) - .clickable { showCreateGroupDialog = true } - .padding(vertical = 16.dp), - contentAlignment = Alignment.Center - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = TwineIcons.Add, - contentDescription = null, - tint = AppTheme.colorScheme.tintedForeground - ) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Box( + modifier = + Modifier.clip(MaterialTheme.shapes.large) + .background(AppTheme.colorScheme.tintedSurface) + .clickable { showCreateGroupDialog = true } + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = TwineIcons.Add, + contentDescription = null, + tint = AppTheme.colorScheme.tintedForeground + ) - Spacer(Modifier.requiredWidth(8.dp)) + Spacer(Modifier.requiredWidth(8.dp)) - Text( - text = LocalStrings.current.groupAddNew, - style = MaterialTheme.typography.labelLarge, - color = AppTheme.colorScheme.tintedForeground, - ) + Text( + text = LocalStrings.current.groupAddNew, + style = MaterialTheme.typography.labelLarge, + color = AppTheme.colorScheme.tintedForeground, + ) + } } } - } - items(groups.itemCount) { index -> - val group = groups[index] - if (group != null) { - FeedGroupItem( - feedGroup = group, - canShowUnreadPostsCount = false, - isInMultiSelectMode = true, - selected = state.selectedGroups.contains(group.id), - onFeedGroupSelected = { - presenter.dispatch(GroupSelectionEvent.OnToggleGroupSelection(group)) - }, - onFeedGroupClick = { - // no-op - } - ) + items(groups.itemCount) { index -> + val group = groups[index] + if (group != null) { + FeedGroupItem( + feedGroup = group, + canShowUnreadPostsCount = false, + isInMultiSelectMode = true, + selected = state.selectedGroups.contains(group.id), + onFeedGroupSelected = { + presenter.dispatch(GroupSelectionEvent.OnToggleGroupSelection(group)) + }, + onFeedGroupClick = { + // no-op + } + ) + } } } - } - Row( - modifier = - Modifier.fillMaxWidth().padding(start = 24.dp, top = 40.dp, end = 24.dp, bottom = 24.dp) - ) { - OutlinedButton( - modifier = Modifier.weight(1f), - colors = - ButtonDefaults.outlinedButtonColors( - containerColor = AppTheme.colorScheme.tintedBackground, - contentColor = AppTheme.colorScheme.tintedForeground - ), - border = BorderStroke(1.dp, AppTheme.colorScheme.tintedHighlight), - onClick = { presenter.dispatch(GroupSelectionEvent.BackClicked) } + Row( + modifier = + Modifier.fillMaxWidth().padding(start = 24.dp, top = 40.dp, end = 24.dp, bottom = 24.dp) ) { - Text(text = LocalStrings.current.buttonGoBack) - } + OutlinedButton( + modifier = Modifier.weight(1f), + colors = + ButtonDefaults.outlinedButtonColors( + containerColor = AppTheme.colorScheme.tintedBackground, + contentColor = AppTheme.colorScheme.tintedForeground + ), + border = BorderStroke(1.dp, AppTheme.colorScheme.tintedHighlight), + onClick = { presenter.dispatch(GroupSelectionEvent.BackClicked) } + ) { + Text(text = LocalStrings.current.buttonGoBack) + } - Spacer(Modifier.requiredWidth(16.dp)) + Spacer(Modifier.requiredWidth(16.dp)) - Button( - modifier = Modifier.weight(1f), - enabled = state.areGroupsSelected, - onClick = { presenter.dispatch(GroupSelectionEvent.OnConfirmGroupSelectionClicked) } - ) { - Text(text = LocalStrings.current.buttonConfirm) + Button( + modifier = Modifier.weight(1f), + enabled = state.areGroupsSelected, + onClick = { presenter.dispatch(GroupSelectionEvent.OnConfirmGroupSelectionClicked) } + ) { + Text(text = LocalStrings.current.buttonConfirm) + } } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt index 7f5c19917..afa57740a 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt @@ -39,13 +39,17 @@ import dev.sasikanth.rss.reader.data.repository.RssRepository import dev.sasikanth.rss.reader.data.repository.SettingsRepository import dev.sasikanth.rss.reader.feeds.FeedsEvent import dev.sasikanth.rss.reader.feeds.FeedsPresenter +import dev.sasikanth.rss.reader.home.ui.FeaturedPostItem +import dev.sasikanth.rss.reader.ui.SeedColorExtractor import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.NTuple4 import dev.sasikanth.rss.reader.utils.getLast24HourStart import dev.sasikanth.rss.reader.utils.getTodayStartInstant +import kotlin.time.measureTimedValue import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -92,6 +96,7 @@ class HomePresenter( private val rssRepository: RssRepository, private val observableActiveSource: ObservableActiveSource, private val settingsRepository: SettingsRepository, + private val seedColorExtractor: SeedColorExtractor, @Assisted componentContext: ComponentContext, @Assisted private val openSearch: () -> Unit, @Assisted private val openBookmarks: () -> Unit, @@ -131,7 +136,8 @@ class HomePresenter( rssRepository = rssRepository, observableActiveSource = observableActiveSource, settingsRepository = settingsRepository, - feedsPresenter = feedsPresenter + feedsPresenter = feedsPresenter, + seedColorExtractor = seedColorExtractor, ) } @@ -166,6 +172,7 @@ class HomePresenter( private val observableActiveSource: ObservableActiveSource, private val settingsRepository: SettingsRepository, private val feedsPresenter: FeedsPresenter, + private val seedColorExtractor: SeedColorExtractor, ) : InstanceKeeper.Instance { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) @@ -267,6 +274,7 @@ class HomePresenter( } } + @OptIn(FlowPreview::class) private fun init() { combine(observableActiveSource.activeSource, settingsRepository.postsType) { activeSource, @@ -314,11 +322,23 @@ class HomePresenter( unreadOnly = unreadOnly, after = postsAfter ) - .onEach { featuredPosts -> - _state.update { it.copy(featuredPosts = featuredPosts.toImmutableList()) } + .map { featuredPosts -> + featuredPosts + .map { postWithMetadata -> + val seedColor = measureTimedValue { + seedColorExtractor.calculateSeedColor(postWithMetadata.imageUrl) + } + + FeaturedPostItem( + postWithMetadata = postWithMetadata, + seedColor = seedColor.value, + ) + } + .toImmutableList() } + .onEach { featuredPosts -> _state.update { it.copy(featuredPosts = featuredPosts) } } .map { featuredPosts -> - val featuredPostsIds = featuredPosts.map { it.id } + val featuredPostsIds = featuredPosts.map { it.postWithMetadata.id } NTuple4(activeSource, postsAfter, unreadOnly, featuredPostsIds) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeState.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeState.kt index 604c86050..e0d412e68 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeState.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomeState.kt @@ -25,12 +25,13 @@ import dev.sasikanth.rss.reader.core.model.local.PostWithMetadata import dev.sasikanth.rss.reader.core.model.local.PostsType import dev.sasikanth.rss.reader.core.model.local.Source import dev.sasikanth.rss.reader.home.HomeLoadingState.Loading +import dev.sasikanth.rss.reader.home.ui.FeaturedPostItem import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @Immutable internal data class HomeState( - val featuredPosts: ImmutableList?, + val featuredPosts: ImmutableList?, val posts: Flow>?, val loadingState: HomeLoadingState, val feedsSheetState: SheetValue, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt index 361302efe..80a7337e8 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/FeaturedPostItem.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -62,6 +63,8 @@ private val featuredImageAspectRatio: Float else -> 1.77f } +@Immutable data class FeaturedPostItem(val postWithMetadata: PostWithMetadata, val seedColor: Int?) + @Composable internal fun FeaturedPostItem( item: PostWithMetadata, 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 8bfdaa2d3..196531e6c 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 @@ -19,7 +19,6 @@ import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -52,16 +51,14 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import coil3.size.Size -import dev.sasikanth.rss.reader.components.LocalDynamicColorState import dev.sasikanth.rss.reader.components.image.AsyncImage import dev.sasikanth.rss.reader.core.model.local.PostWithMetadata import dev.sasikanth.rss.reader.ui.AppTheme +import dev.sasikanth.rss.reader.ui.LocalDynamicColorState import dev.sasikanth.rss.reader.util.canBlurImage -import dev.sasikanth.rss.reader.utils.Constants.EPSILON import dev.sasikanth.rss.reader.utils.LocalWindowSizeClass import kotlin.math.absoluteValue import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.collectLatest private val featuredImageBackgroundAspectRatio: Float @Composable @@ -89,9 +86,10 @@ private val featuredGradientBackgroundAspectRatio: Float @Composable internal fun FeaturedSection( paddingValues: PaddingValues, - featuredPosts: ImmutableList, + featuredPosts: ImmutableList, pagerState: PagerState, featuredItemBlurEnabled: Boolean, + useDarkTheme: Boolean, modifier: Modifier = Modifier, onItemClick: (PostWithMetadata) -> Unit, onPostBookmarkClick: (PostWithMetadata) -> Unit, @@ -99,100 +97,84 @@ internal fun FeaturedSection( onPostSourceClick: (String) -> Unit, onTogglePostReadClick: (String, Boolean) -> Unit, ) { - Box(modifier = modifier) { - if (featuredPosts.isNotEmpty()) { - val layoutDirection = LocalLayoutDirection.current - val dynamicColorState = LocalDynamicColorState.current + val layoutDirection = LocalLayoutDirection.current + val dynamicColorState = LocalDynamicColorState.current - val systemBarsPaddingValues = - WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() - val systemBarsStartPadding = systemBarsPaddingValues.calculateStartPadding(layoutDirection) - val systemBarsEndPadding = systemBarsPaddingValues.calculateEndPadding(layoutDirection) + val systemBarsPaddingValues = + WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() + val systemBarsStartPadding = systemBarsPaddingValues.calculateStartPadding(layoutDirection) + val systemBarsEndPadding = systemBarsPaddingValues.calculateEndPadding(layoutDirection) - val systemBarsHorizontalPadding = - if (systemBarsStartPadding > systemBarsEndPadding) { - systemBarsStartPadding - } else { - systemBarsEndPadding - } - - LaunchedEffect(pagerState, featuredPosts) { - dynamicColorState.onContentChange(featuredPosts.map { it.imageUrl!! }) - - snapshotFlow { - val settledPage = pagerState.settledPage - val offset = - if (settledPage in 0..pagerState.pageCount) { - pagerState.getOffsetFractionForPage(settledPage).coerceIn(-1f, 1f) - } else { - 0f - } - - settledPage to - when { - (settledPage == 0 && offset < -EPSILON) || - (settledPage == featuredPosts.lastIndex && offset > EPSILON) -> { - offset.coerceAtMost(0f) - } - else -> offset - } - } - .collectLatest { (settledPage, offset) -> - val previousImageUrl = featuredPosts.getOrNull(settledPage - 1)?.imageUrl - val currentImageUrl = featuredPosts[settledPage].imageUrl!! - val nextImageUrl = featuredPosts.getOrNull(settledPage + 1)?.imageUrl + val systemBarsHorizontalPadding = + if (systemBarsStartPadding > systemBarsEndPadding) { + systemBarsStartPadding + } else { + systemBarsEndPadding + } - dynamicColorState.updateOffset(previousImageUrl, currentImageUrl, nextImageUrl, offset) - } + LaunchedEffect(pagerState) { + snapshotFlow { + val settledPage = pagerState.settledPage + try { + pagerState.getOffsetFractionForPage(settledPage) + } catch (e: Throwable) { + 0f + } } + .collect { offset -> + val currentItem = featuredPosts[pagerState.settledPage] + val targetItem = featuredPosts[pagerState.targetPage] - Box { - // Duplicated pager setup for background, to avoid pager content padding - // from the items - val pagerFlingBehavior = - PagerDefaults.flingBehavior( - state = pagerState, - snapAnimationSpec = spring(stiffness = Spring.StiffnessVeryLow) - ) - - FeaturedSectionBackground( - modifier = Modifier.matchParentSize(), - state = pagerState, - featuredPosts = featuredPosts, - flingBehavior = pagerFlingBehavior, - featuredItemBlurEnabled = featuredItemBlurEnabled, + dynamicColorState.animate( + fromSeedColor = Color(currentItem.seedColor!!), + toSeedColor = Color(targetItem.seedColor!!), + progress = offset ) + } + } + + Box(modifier) { + // Duplicated pager setup for background, to avoid pager content padding + // from the items + if (useDarkTheme) { + FeaturedSectionBackground( + modifier = Modifier.matchParentSize(), + state = pagerState, + featuredPosts = featuredPosts, + featuredItemBlurEnabled = featuredItemBlurEnabled, + ) + } - HorizontalPager( + HorizontalPager( + state = pagerState, + verticalAlignment = Alignment.Top, + contentPadding = + PaddingValues( + start = systemBarsHorizontalPadding + 24.dp, + top = 8.dp + paddingValues.calculateTopPadding(), + end = systemBarsHorizontalPadding + 24.dp, + bottom = 24.dp + ), + pageSpacing = 16.dp, + flingBehavior = + PagerDefaults.flingBehavior( state = pagerState, - verticalAlignment = Alignment.Top, - contentPadding = - PaddingValues( - start = systemBarsHorizontalPadding + 24.dp, - top = 8.dp + paddingValues.calculateTopPadding(), - end = systemBarsHorizontalPadding + 24.dp, - bottom = 24.dp - ), - pageSpacing = 16.dp, - flingBehavior = pagerFlingBehavior, - ) { page -> - val featuredPost = featuredPosts.getOrNull(page) - if (featuredPost != null) { - Box { - FeaturedPostItem( - item = featuredPost, - page = page, - pagerState = pagerState, - onClick = { onItemClick(featuredPost) }, - onBookmarkClick = { onPostBookmarkClick(featuredPost) }, - onCommentsClick = { onPostCommentsClick(featuredPost.commentsLink!!) }, - onSourceClick = { onPostSourceClick(featuredPost.sourceId) }, - onTogglePostReadClick = { - onTogglePostReadClick(featuredPost.id, featuredPost.read) - } - ) - } - } + snapAnimationSpec = spring(stiffness = Spring.StiffnessVeryLow) + ), + ) { page -> + val featuredPost = featuredPosts.getOrNull(page)?.postWithMetadata + if (featuredPost != null) { + Box { + FeaturedPostItem( + item = featuredPost, + page = page, + pagerState = pagerState, + onClick = { onItemClick(featuredPost) }, + onBookmarkClick = { onPostBookmarkClick(featuredPost) }, + onCommentsClick = { onPostCommentsClick(featuredPost.commentsLink!!) }, + onSourceClick = { onPostSourceClick(featuredPost.sourceId) }, + onTogglePostReadClick = { onTogglePostReadClick(featuredPost.id, featuredPost.read) } + ) } } } @@ -203,8 +185,7 @@ internal fun FeaturedSection( @Composable private fun FeaturedSectionBackground( state: PagerState, - featuredPosts: ImmutableList, - flingBehavior: SnapFlingBehavior, + featuredPosts: ImmutableList, featuredItemBlurEnabled: Boolean, modifier: Modifier = Modifier, ) { @@ -212,7 +193,6 @@ private fun FeaturedSectionBackground( modifier = modifier, state = state, verticalAlignment = Alignment.Top, - flingBehavior = flingBehavior ) { page -> val featuredPost = featuredPosts.getOrNull(page) featuredPost?.let { @@ -284,11 +264,11 @@ private fun FeaturedSectionGradientBackground(modifier: Modifier = Modifier) { @Composable private fun FeaturedSectionBlurredBackground( - post: PostWithMetadata, + post: FeaturedPostItem, modifier: Modifier = Modifier ) { AsyncImage( - url = post.imageUrl!!, + url = post.postWithMetadata.imageUrl!!, modifier = Modifier.aspectRatio(featuredImageBackgroundAspectRatio) .graphicsLayer { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt index 7edb935fd..c971d9e01 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt @@ -16,6 +16,7 @@ package dev.sasikanth.rss.reader.home.ui import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight @@ -59,6 +61,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.text.style.TextAlign @@ -68,7 +71,6 @@ import androidx.compose.ui.util.fastMaxBy import androidx.paging.LoadState import app.cash.paging.compose.collectAsLazyPagingItems import dev.sasikanth.rss.reader.components.CompactFloatingActionButton -import dev.sasikanth.rss.reader.components.LocalDynamicColorState import dev.sasikanth.rss.reader.feeds.ui.FeedsBottomSheet import dev.sasikanth.rss.reader.home.HomeEvent import dev.sasikanth.rss.reader.home.HomePresenter @@ -77,6 +79,8 @@ import dev.sasikanth.rss.reader.resources.icons.Feed 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.ui.DynamicContentTheme +import dev.sasikanth.rss.reader.ui.LocalDynamicColorState import dev.sasikanth.rss.reader.utils.inverse import kotlinx.coroutines.launch @@ -85,7 +89,12 @@ private val BOTTOM_SHEET_CORNER_SIZE = 32.dp @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun HomeScreen(homePresenter: HomePresenter, modifier: Modifier = Modifier) { +internal fun HomeScreen( + homePresenter: HomePresenter, + useDarkTheme: Boolean = false, + modifier: Modifier = Modifier, + onBottomSheetStateChanged: (SheetValue) -> Unit, +) { val coroutineScope = rememberCoroutineScope() val state by homePresenter.state.collectAsState() val feedsState by homePresenter.feedsPresenter.state.collectAsState() @@ -100,6 +109,8 @@ internal fun HomeScreen(homePresenter: HomePresenter, modifier: Modifier = Modif homePresenter.dispatch(HomeEvent.FeedsSheetStateChanged(SheetValue.PartiallyExpanded)) } + onBottomSheetStateChanged(it) + true } ) @@ -117,6 +128,8 @@ internal fun HomeScreen(homePresenter: HomePresenter, modifier: Modifier = Modif val sheetPeekHeight = BOTTOM_SHEET_PEEK_HEIGHT + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + val featuredPosts = state.featuredPosts + val dynamicColorState = LocalDynamicColorState.current LaunchedEffect(state.feedsSheetState) { if ( @@ -127,129 +140,136 @@ internal fun HomeScreen(homePresenter: HomePresenter, modifier: Modifier = Modif } } - BottomSheetScaffold( - modifier = modifier, - scaffoldState = bottomSheetScaffoldState, - content = { _ -> - Box { - val featuredPosts = state.featuredPosts - val posts = state.posts?.collectAsLazyPagingItems() - val hasFeeds = state.hasFeeds - val dynamicColorState = LocalDynamicColorState.current - - LaunchedEffect(featuredPosts) { - if (featuredPosts.isNullOrEmpty()) { - dynamicColorState.reset() - } - } + LaunchedEffect(featuredPosts) { + if (featuredPosts.isNullOrEmpty()) { + dynamicColorState.reset() + } + } - val swipeRefreshState = - rememberPullRefreshState( - refreshing = state.isRefreshing, - onRefresh = { homePresenter.dispatch(HomeEvent.OnSwipeToRefresh) } - ) - val canSwipeToRefresh = hasFeeds == true + DynamicContentTheme(useDarkTheme = true) { + BottomSheetScaffold( + modifier = modifier, + scaffoldState = bottomSheetScaffoldState, + content = { _ -> + DynamicContentTheme(useDarkTheme = useDarkTheme) { + Box( + modifier = + Modifier.fillMaxWidth().background(AppTheme.colorScheme.surfaceContainerLowest) + ) { + val posts = state.posts?.collectAsLazyPagingItems() + val hasFeeds = state.hasFeeds + val swipeRefreshState = + rememberPullRefreshState( + refreshing = state.isRefreshing, + onRefresh = { homePresenter.dispatch(HomeEvent.OnSwipeToRefresh) } + ) + val canSwipeToRefresh = hasFeeds == true - HomeScreenContentLayout( - modifier = Modifier.pullRefresh(state = swipeRefreshState, enabled = canSwipeToRefresh), - homeTopAppBar = { - HomeTopAppBar( - source = state.activeSource, - postsType = state.postsType, - listState = listState, - hasFeeds = hasFeeds, - hasUnreadPosts = state.hasUnreadPosts, - onSearchClicked = { homePresenter.dispatch(HomeEvent.SearchClicked) }, - onBookmarksClicked = { homePresenter.dispatch(HomeEvent.BookmarksClicked) }, - onSettingsClicked = { homePresenter.dispatch(HomeEvent.SettingsClicked) }, - onPostTypeChanged = { homePresenter.dispatch(HomeEvent.OnPostsTypeChanged(it)) }, - onMarkPostsAsRead = { homePresenter.dispatch(HomeEvent.MarkPostsAsRead(it)) } - ) - }, - body = { paddingValues -> - Box { - when { - hasFeeds == null || (posts == null || featuredPosts == null) -> { - // no-op - } - featuredPosts.isNotEmpty() || - (posts.itemCount > 0 || posts.loadState.refresh == LoadState.Loading) -> { - PostsList( - paddingValues = paddingValues, - featuredPosts = featuredPosts, - posts = posts, - featuredItemBlurEnabled = state.featuredItemBlurEnabled, - listState = listState, - featuredPostsPagerState = featuredPostsPagerState, - onPostClicked = { homePresenter.dispatch(HomeEvent.OnPostClicked(it)) }, - onPostBookmarkClick = { - homePresenter.dispatch(HomeEvent.OnPostBookmarkClick(it)) - }, - onPostCommentsClick = { commentsLink -> - coroutineScope.launch { linkHandler.openLink(commentsLink) } - }, - onPostSourceClick = { feedId -> - homePresenter.dispatch(HomeEvent.OnPostSourceClicked(feedId)) - }, - onTogglePostReadClick = { postId, postRead -> - homePresenter.dispatch(HomeEvent.TogglePostReadStatus(postId, postRead)) + HomeScreenContentLayout( + modifier = + Modifier.pullRefresh(state = swipeRefreshState, enabled = canSwipeToRefresh), + homeTopAppBar = { + HomeTopAppBar( + source = state.activeSource, + postsType = state.postsType, + listState = listState, + hasFeeds = hasFeeds, + hasUnreadPosts = state.hasUnreadPosts, + onSearchClicked = { homePresenter.dispatch(HomeEvent.SearchClicked) }, + onBookmarksClicked = { homePresenter.dispatch(HomeEvent.BookmarksClicked) }, + onSettingsClicked = { homePresenter.dispatch(HomeEvent.SettingsClicked) }, + onPostTypeChanged = { homePresenter.dispatch(HomeEvent.OnPostsTypeChanged(it)) }, + onMarkPostsAsRead = { homePresenter.dispatch(HomeEvent.MarkPostsAsRead(it)) } + ) + }, + body = { paddingValues -> + Box { + when { + hasFeeds == null || (posts == null || featuredPosts == null) -> { + // no-op } + featuredPosts.isNotEmpty() || + (posts.itemCount > 0 || posts.loadState.refresh == LoadState.Loading) -> { + PostsList( + paddingValues = paddingValues, + featuredPosts = featuredPosts, + posts = posts, + featuredItemBlurEnabled = state.featuredItemBlurEnabled, + useDarkTheme = useDarkTheme, + listState = listState, + featuredPostsPagerState = featuredPostsPagerState, + onPostClicked = { homePresenter.dispatch(HomeEvent.OnPostClicked(it)) }, + onPostBookmarkClick = { + homePresenter.dispatch(HomeEvent.OnPostBookmarkClick(it)) + }, + onPostCommentsClick = { commentsLink -> + coroutineScope.launch { linkHandler.openLink(commentsLink) } + }, + onPostSourceClick = { feedId -> + homePresenter.dispatch(HomeEvent.OnPostSourceClicked(feedId)) + }, + onTogglePostReadClick = { postId, postRead -> + homePresenter.dispatch(HomeEvent.TogglePostReadStatus(postId, postRead)) + } + ) + } + !hasFeeds -> { + NoFeeds { coroutineScope.launch { bottomSheetState.expand() } } + } + featuredPosts.isEmpty() && posts.itemCount == 0 -> { + NoNewPosts() + } + } + + PullRefreshIndicator( + refreshing = state.isRefreshing, + state = swipeRefreshState, + modifier = + Modifier.windowInsetsPadding(WindowInsets.statusBars) + .align(Alignment.TopCenter) ) } - !hasFeeds -> { - NoFeeds { coroutineScope.launch { bottomSheetState.expand() } } - } - featuredPosts.isEmpty() && posts.itemCount == 0 -> { - NoNewPosts() - } - } + }, + ) - PullRefreshIndicator( - refreshing = state.isRefreshing, - state = swipeRefreshState, - modifier = - Modifier.windowInsetsPadding(WindowInsets.statusBars).align(Alignment.TopCenter) - ) + CompactFloatingActionButton( + label = LocalStrings.current.scrollToTop, + visible = showScrollToTop, + modifier = Modifier.padding(end = 16.dp, bottom = sheetPeekHeight + 16.dp), + ) { + listState.animateScrollToItem(0) } - }, - ) - - CompactFloatingActionButton( - label = LocalStrings.current.scrollToTop, - visible = showScrollToTop, - modifier = Modifier.padding(end = 16.dp, bottom = sheetPeekHeight + 16.dp), - ) { - listState.animateScrollToItem(0) - } - } - }, - sheetContent = { - FeedsBottomSheet( - feedsPresenter = homePresenter.feedsPresenter, - bottomSheetProgress = bottomSheetProgress, - closeSheet = { coroutineScope.launch { bottomSheetState.partialExpand() } }, - selectedFeedChanged = { - coroutineScope.launch { - listState.scrollToItem(0) - featuredPostsPagerState.scrollToPage(0) } } - ) - }, - containerColor = AppTheme.colorScheme.surfaceContainerLowest, - sheetContainerColor = AppTheme.colorScheme.tintedBackground, - sheetContentColor = AppTheme.colorScheme.tintedForeground, - sheetShadowElevation = 0.dp, - sheetTonalElevation = 0.dp, - sheetPeekHeight = sheetPeekHeight, - sheetShape = - RoundedCornerShape( - topStart = BOTTOM_SHEET_CORNER_SIZE * bottomSheetProgress.inverse(), - topEnd = BOTTOM_SHEET_CORNER_SIZE * bottomSheetProgress.inverse() - ), - sheetSwipeEnabled = !feedsState.isInMultiSelectMode, - sheetDragHandle = null - ) + }, + sheetContent = { + FeedsBottomSheet( + feedsPresenter = homePresenter.feedsPresenter, + bottomSheetProgress = bottomSheetProgress, + closeSheet = { coroutineScope.launch { bottomSheetState.partialExpand() } }, + selectedFeedChanged = { + coroutineScope.launch { + listState.scrollToItem(0) + featuredPostsPagerState.scrollToPage(0) + } + } + ) + }, + containerColor = Color.Transparent, + sheetContainerColor = AppTheme.colorScheme.tintedBackground, + sheetContentColor = AppTheme.colorScheme.tintedForeground, + sheetShadowElevation = 0.dp, + sheetTonalElevation = 0.dp, + sheetPeekHeight = sheetPeekHeight, + sheetShape = + RoundedCornerShape( + topStart = BOTTOM_SHEET_CORNER_SIZE * bottomSheetProgress.inverse(), + topEnd = BOTTOM_SHEET_CORNER_SIZE * bottomSheetProgress.inverse() + ), + sheetSwipeEnabled = !feedsState.isInMultiSelectMode, + sheetDragHandle = null + ) + } } @Composable 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 a5fae216c..f9969c4cb 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 @@ -69,9 +69,10 @@ private val postListPadding @OptIn(ExperimentalFoundationApi::class) internal fun PostsList( paddingValues: PaddingValues, - featuredPosts: ImmutableList, + featuredPosts: ImmutableList, posts: LazyPagingItems, featuredItemBlurEnabled: Boolean, + useDarkTheme: Boolean, listState: LazyListState, featuredPostsPagerState: PagerState, onPostClicked: (post: PostWithMetadata) -> Unit, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt index 4300d425c..38223265a 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt @@ -20,6 +20,7 @@ import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @@ -33,11 +34,12 @@ import twine.shared.generated.resources.golos_regular @Composable internal fun AppTheme( - appColorScheme: AppColorScheme = AppTheme.colorScheme, + useDarkTheme: Boolean = false, + appColorScheme: AppColorScheme, content: @Composable () -> Unit ) { MaterialTheme( - colorScheme = darkColorScheme(), + colorScheme = if (useDarkTheme) darkColorScheme() else lightColorScheme(), typography = typography(GolosFontFamily), ) { CompositionLocalProvider( diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt new file mode 100644 index 000000000..c552f89d6 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2024 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.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.graphics.toArgb +import dev.sasikanth.material.color.utilities.dynamiccolor.DynamicColor +import dev.sasikanth.material.color.utilities.dynamiccolor.MaterialDynamicColors +import dev.sasikanth.material.color.utilities.dynamiccolor.ToneDeltaConstraint +import dev.sasikanth.material.color.utilities.dynamiccolor.TonePolarity +import dev.sasikanth.material.color.utilities.hct.Hct +import dev.sasikanth.material.color.utilities.scheme.SchemeContent +import dev.sasikanth.rss.reader.utils.Constants.EPSILON +import dev.sasikanth.rss.reader.utils.inverse +import kotlin.math.absoluteValue + +@Composable +internal fun DynamicContentTheme( + state: DynamicColorState? = null, + useDarkTheme: Boolean = false, + content: @Composable () -> Unit +) { + val dynamicColorState = state ?: LocalDynamicColorState.current + val colorScheme = + if (useDarkTheme) { + dynamicColorState.darkAppColorScheme + } else { + dynamicColorState.lightAppColorScheme + } + + CompositionLocalProvider(LocalDynamicColorState provides dynamicColorState) { + AppTheme(useDarkTheme = useDarkTheme, appColorScheme = colorScheme, content = content) + } +} + +@Composable +internal fun rememberDynamicColorState( + defaultLightAppColorScheme: AppColorScheme, + defaultDarkAppColorScheme: AppColorScheme, +): DynamicColorState { + return remember { + DynamicColorState( + defaultLightAppColorScheme = defaultLightAppColorScheme, + defaultDarkAppColorScheme = defaultDarkAppColorScheme, + ) + } +} + +@Stable +internal class DynamicColorState( + private val defaultLightAppColorScheme: AppColorScheme, + private val defaultDarkAppColorScheme: AppColorScheme, +) { + var lightAppColorScheme by mutableStateOf(defaultLightAppColorScheme) + private set + + var darkAppColorScheme by mutableStateOf(defaultDarkAppColorScheme) + private set + + fun animate(fromSeedColor: Color, toSeedColor: Color, progress: Float) { + val normalizedProgress = + if (progress < -EPSILON) { + progress.absoluteValue.inverse() + } else { + progress + } + + val startLightColors = + generateDynamicColorsFromSeedColor(useDarkTheme = false, seedColor = fromSeedColor) + + val startDarkColors = + generateDynamicColorsFromSeedColor(useDarkTheme = true, seedColor = fromSeedColor) + + val endLightColors = + generateDynamicColorsFromSeedColor(useDarkTheme = false, seedColor = toSeedColor) + + val endDarkColors = + generateDynamicColorsFromSeedColor(useDarkTheme = true, seedColor = toSeedColor) + + lightAppColorScheme = + defaultLightAppColorScheme.animate( + startColors = startLightColors, + endColors = endLightColors, + progress = normalizedProgress + ) + + darkAppColorScheme = + defaultDarkAppColorScheme.animate( + startColors = startDarkColors, + endColors = endDarkColors, + progress = normalizedProgress, + ) + } + + fun reset() { + lightAppColorScheme = defaultLightAppColorScheme + darkAppColorScheme = defaultDarkAppColorScheme + } + + private fun generateDynamicColorsFromSeedColor( + useDarkTheme: Boolean, + seedColor: Color + ): DynamicColors { + val scheme = + SchemeContent( + sourceColorHct = Hct.fromInt(seedColor.toArgb()), + isDark = useDarkTheme, + contrastLevel = 0.0 + ) + val dynamicColors = MaterialDynamicColors() + + return DynamicColors( + tintedBackground = + DynamicColor.fromPalette( + palette = { s -> s.primaryPalette }, + tone = { s -> if (s.isDark) 10.0 else 99.0 }, + background = { s -> dynamicColors.highestSurface(s) }, + toneDeltaConstraint = { s -> + ToneDeltaConstraint( + MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, + dynamicColors.primaryContainer(), + if (s.isDark) TonePolarity.DARKER else TonePolarity.LIGHTER + ) + } + ) + .toColor(scheme), + tintedSurface = + DynamicColor.fromPalette( + palette = { s -> s.primaryPalette }, + tone = { s -> if (s.isDark) 20.0 else 95.0 }, + background = { s -> dynamicColors.highestSurface(s) }, + toneDeltaConstraint = { s -> + ToneDeltaConstraint( + MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, + dynamicColors.primaryContainer(), + if (s.isDark) TonePolarity.DARKER else TonePolarity.LIGHTER + ) + } + ) + .toColor(scheme), + tintedForeground = + DynamicColor.fromPalette( + palette = { s -> s.primaryPalette }, + tone = { s -> if (s.isDark) 80.0 else 40.0 }, + background = { s -> dynamicColors.highestSurface(s) }, + toneDeltaConstraint = { s -> + ToneDeltaConstraint( + MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, + dynamicColors.primaryContainer(), + if (s.isDark) TonePolarity.DARKER else TonePolarity.LIGHTER + ) + } + ) + .toColor(scheme), + tintedHighlight = + DynamicColor.fromPalette( + palette = { s -> s.primaryPalette }, + tone = { s -> if (s.isDark) 40.0 else 80.0 }, + background = { s -> dynamicColors.highestSurface(s) }, + toneDeltaConstraint = { s -> + ToneDeltaConstraint( + MaterialDynamicColors.CONTAINER_ACCENT_TONE_DELTA, + dynamicColors.primaryContainer(), + if (s.isDark) TonePolarity.DARKER else TonePolarity.LIGHTER + ) + } + ) + .toColor(scheme), + outline = dynamicColors.outline().toColor(scheme), + outlineVariant = dynamicColors.outlineVariant().toColor(scheme), + surface = dynamicColors.surface().toColor(scheme), + onSurface = dynamicColors.onSurface().toColor(scheme), + onSurfaceVariant = dynamicColors.onSurfaceVariant().toColor(scheme), + surfaceContainer = dynamicColors.surfaceContainer().toColor(scheme), + surfaceContainerLow = dynamicColors.surfaceContainerLow().toColor(scheme), + surfaceContainerLowest = dynamicColors.surfaceContainerLowest().toColor(scheme), + surfaceContainerHigh = dynamicColors.surfaceContainerHigh().toColor(scheme), + surfaceContainerHighest = dynamicColors.surfaceContainerHighest().toColor(scheme), + ) + } + + private fun DynamicColor.toColor(scheme: SchemeContent): Color { + return Color(getArgb(scheme)) + } +} + +@Immutable +data class DynamicColors( + val tintedBackground: Color, + val tintedSurface: Color, + val tintedForeground: Color, + val tintedHighlight: Color, + val outline: Color, + val outlineVariant: Color, + val surface: Color, + val onSurface: Color, + val onSurfaceVariant: Color, + val surfaceContainer: Color, + val surfaceContainerLow: Color, + val surfaceContainerLowest: Color, + val surfaceContainerHigh: Color, + val surfaceContainerHighest: Color, +) { + + companion object { + + fun fromColorScheme(colorScheme: AppColorScheme) = + DynamicColors( + tintedBackground = colorScheme.tintedBackground, + tintedSurface = colorScheme.tintedSurface, + tintedForeground = colorScheme.tintedForeground, + tintedHighlight = colorScheme.tintedHighlight, + outline = colorScheme.outline, + outlineVariant = colorScheme.outlineVariant, + surface = colorScheme.surface, + onSurface = colorScheme.onSurface, + onSurfaceVariant = colorScheme.onSurfaceVariant, + surfaceContainer = colorScheme.surfaceContainer, + surfaceContainerLow = colorScheme.surfaceContainerLow, + surfaceContainerLowest = colorScheme.surfaceContainerLowest, + surfaceContainerHigh = colorScheme.surfaceContainerHigh, + surfaceContainerHighest = colorScheme.surfaceContainerHighest, + ) + } + + fun toColorScheme(defaultColorScheme: AppColorScheme) = + AppColorScheme( + tintedBackground = tintedBackground, + tintedSurface = tintedSurface, + tintedForeground = tintedForeground, + tintedHighlight = tintedHighlight, + outline = outline, + outlineVariant = outlineVariant, + surface = surface, + onSurface = onSurface, + onSurfaceVariant = onSurfaceVariant, + surfaceContainer = surfaceContainer, + surfaceContainerLow = surfaceContainerLow, + surfaceContainerLowest = surfaceContainerLowest, + surfaceContainerHigh = surfaceContainerHigh, + surfaceContainerHighest = surfaceContainerHighest, + textEmphasisHigh = defaultColorScheme.textEmphasisHigh, + textEmphasisMed = defaultColorScheme.textEmphasisMed, + ) +} + +fun AppColorScheme.animate( + startColors: DynamicColors?, + endColors: DynamicColors?, + progress: Float, +): AppColorScheme { + if (startColors == null || endColors == null) return this + + return copy( + tintedBackground = + lerp( + start = startColors.tintedBackground, + stop = endColors.tintedBackground, + fraction = progress + ), + tintedSurface = + lerp(start = startColors.tintedSurface, stop = endColors.tintedSurface, fraction = progress), + tintedForeground = + lerp( + start = startColors.tintedForeground, + stop = endColors.tintedForeground, + fraction = progress + ), + tintedHighlight = + lerp( + start = startColors.tintedHighlight, + stop = endColors.tintedHighlight, + fraction = progress + ), + outline = lerp(start = startColors.outline, stop = endColors.outline, fraction = progress), + outlineVariant = + lerp( + start = startColors.outlineVariant, + stop = endColors.outlineVariant, + fraction = progress + ), + surface = lerp(start = startColors.surface, stop = endColors.surface, fraction = progress), + onSurface = + lerp(start = startColors.onSurface, stop = endColors.onSurface, fraction = progress), + onSurfaceVariant = + lerp( + start = startColors.onSurfaceVariant, + stop = endColors.onSurfaceVariant, + fraction = progress + ), + surfaceContainer = + lerp( + start = startColors.surfaceContainer, + stop = endColors.surfaceContainer, + fraction = progress + ), + surfaceContainerLow = + lerp( + start = startColors.surfaceContainerLow, + stop = endColors.surfaceContainerLow, + fraction = progress + ), + surfaceContainerLowest = + lerp( + start = startColors.surfaceContainerLowest, + stop = endColors.surfaceContainerLowest, + fraction = progress + ), + surfaceContainerHigh = + lerp( + start = startColors.surfaceContainerHigh, + stop = endColors.surfaceContainerHigh, + fraction = progress + ), + surfaceContainerHighest = + lerp( + start = startColors.surfaceContainerHighest, + stop = endColors.surfaceContainerHighest, + fraction = progress + ), + ) +} + +internal val LocalDynamicColorState = + staticCompositionLocalOf { + throw NullPointerException("Please provide a dynamic color state") + } diff --git a/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/HomeViewController.kt b/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/HomeViewController.kt index 4e59283d0..977c7e87b 100644 --- a/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/HomeViewController.kt +++ b/shared/src/iosMain/kotlin/dev/sasikanth/rss/reader/HomeViewController.kt @@ -31,13 +31,16 @@ typealias HomeViewController = (backDispatcher: BackDispatcher) -> UIViewControl @OptIn(ExperimentalDecomposeApi::class) @Inject -fun HomeViewController(app: App, @Assisted backDispatcher: BackDispatcher) = - ComposeUIViewController(configure = { onFocusBehavior = OnFocusBehavior.DoNothing }) { +fun HomeViewController(app: App, @Assisted backDispatcher: BackDispatcher): UIViewController { + return ComposeUIViewController(configure = { onFocusBehavior = OnFocusBehavior.DoNothing }) { PredictiveBackGestureOverlay( backDispatcher = backDispatcher, backIcon = null, modifier = Modifier.fillMaxSize() ) { - app() + app { + // no-op + } } } +} From 2476cc38fc89e5ce242d05210980e27db0cbb412 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 06:13:05 +0530 Subject: [PATCH 09/21] Use LRU cache for storing dynamic colors during animation Since the generated colors will be same given the seed color. It makes sense to keep them in a cache and access them during the animation. --- .../rss/reader/ui/DynamicContentTheme.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt index c552f89d6..3e1b302b0 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -15,6 +15,7 @@ */ package dev.sasikanth.rss.reader.ui +import androidx.collection.lruCache import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -80,6 +81,8 @@ internal class DynamicColorState( var darkAppColorScheme by mutableStateOf(defaultDarkAppColorScheme) private set + private val cache = lruCache(maxSize = 10) + fun animate(fromSeedColor: Color, toSeedColor: Color, progress: Float) { val normalizedProgress = if (progress < -EPSILON) { @@ -89,16 +92,20 @@ internal class DynamicColorState( } val startLightColors = - generateDynamicColorsFromSeedColor(useDarkTheme = false, seedColor = fromSeedColor) + cache["light_$fromSeedColor"] + ?: generateDynamicColorsFromSeedColor(useDarkTheme = false, seedColor = fromSeedColor) val startDarkColors = - generateDynamicColorsFromSeedColor(useDarkTheme = true, seedColor = fromSeedColor) + cache["dark_$fromSeedColor"] + ?: generateDynamicColorsFromSeedColor(useDarkTheme = true, seedColor = fromSeedColor) val endLightColors = - generateDynamicColorsFromSeedColor(useDarkTheme = false, seedColor = toSeedColor) + cache["light_$toSeedColor"] + ?: generateDynamicColorsFromSeedColor(useDarkTheme = false, seedColor = toSeedColor) val endDarkColors = - generateDynamicColorsFromSeedColor(useDarkTheme = true, seedColor = toSeedColor) + cache["dark_$toSeedColor"] + ?: generateDynamicColorsFromSeedColor(useDarkTheme = true, seedColor = toSeedColor) lightAppColorScheme = defaultLightAppColorScheme.animate( From a45b939dbc60bb69ad3f25b72f7690407110bf2b Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 06:13:23 +0530 Subject: [PATCH 10/21] Remove unused code in `DynamicContentTheme` --- .../rss/reader/ui/DynamicContentTheme.kt | 44 +------------------ 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt index 3e1b302b0..d8a73252b 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -230,49 +230,7 @@ data class DynamicColors( val surfaceContainerLowest: Color, val surfaceContainerHigh: Color, val surfaceContainerHighest: Color, -) { - - companion object { - - fun fromColorScheme(colorScheme: AppColorScheme) = - DynamicColors( - tintedBackground = colorScheme.tintedBackground, - tintedSurface = colorScheme.tintedSurface, - tintedForeground = colorScheme.tintedForeground, - tintedHighlight = colorScheme.tintedHighlight, - outline = colorScheme.outline, - outlineVariant = colorScheme.outlineVariant, - surface = colorScheme.surface, - onSurface = colorScheme.onSurface, - onSurfaceVariant = colorScheme.onSurfaceVariant, - surfaceContainer = colorScheme.surfaceContainer, - surfaceContainerLow = colorScheme.surfaceContainerLow, - surfaceContainerLowest = colorScheme.surfaceContainerLowest, - surfaceContainerHigh = colorScheme.surfaceContainerHigh, - surfaceContainerHighest = colorScheme.surfaceContainerHighest, - ) - } - - fun toColorScheme(defaultColorScheme: AppColorScheme) = - AppColorScheme( - tintedBackground = tintedBackground, - tintedSurface = tintedSurface, - tintedForeground = tintedForeground, - tintedHighlight = tintedHighlight, - outline = outline, - outlineVariant = outlineVariant, - surface = surface, - onSurface = onSurface, - onSurfaceVariant = onSurfaceVariant, - surfaceContainer = surfaceContainer, - surfaceContainerLow = surfaceContainerLow, - surfaceContainerLowest = surfaceContainerLowest, - surfaceContainerHigh = surfaceContainerHigh, - surfaceContainerHighest = surfaceContainerHighest, - textEmphasisHigh = defaultColorScheme.textEmphasisHigh, - textEmphasisMed = defaultColorScheme.textEmphasisMed, - ) -} +) fun AppColorScheme.animate( startColors: DynamicColors?, From 23dbce7c9e00ce29f116ad9079a375d35f1a12bf Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 06:27:29 +0530 Subject: [PATCH 11/21] Reduce cache size in `DynamicContentTheme` --- .../kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt index d8a73252b..da846b657 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -81,7 +81,7 @@ internal class DynamicColorState( var darkAppColorScheme by mutableStateOf(defaultDarkAppColorScheme) private set - private val cache = lruCache(maxSize = 10) + private val cache = lruCache(maxSize = 4) fun animate(fromSeedColor: Color, toSeedColor: Color, progress: Float) { val normalizedProgress = From 337dde7fac69614431190c7fb648a208252c0426 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 06:34:56 +0530 Subject: [PATCH 12/21] Use pager offset for calculating from and to seed colors in `FeaturedSection` --- .../rss/reader/home/ui/FeaturedSection.kt | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 196531e6c..a5d11d558 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 @@ -56,6 +56,7 @@ import dev.sasikanth.rss.reader.core.model.local.PostWithMetadata import dev.sasikanth.rss.reader.ui.AppTheme import dev.sasikanth.rss.reader.ui.LocalDynamicColorState import dev.sasikanth.rss.reader.util.canBlurImage +import dev.sasikanth.rss.reader.utils.Constants.EPSILON import dev.sasikanth.rss.reader.utils.LocalWindowSizeClass import kotlin.math.absoluteValue import kotlinx.collections.immutable.ImmutableList @@ -122,12 +123,31 @@ internal fun FeaturedSection( } } .collect { offset -> + // The default snap position of the pager is 0.5f, that means the targetPage + // state only changes after reaching half way point. We instead want it to scale + // as we start swiping. + // + // Instead of using EPSILON for snap threshold, we are doing that calculation + // as the page offset changes + // val currentItem = featuredPosts[pagerState.settledPage] - val targetItem = featuredPosts[pagerState.targetPage] + val fromItem = + if (offset < -EPSILON) { + featuredPosts[pagerState.settledPage - 1] + } else { + currentItem + } + + val toItem = + if (offset > EPSILON) { + featuredPosts[pagerState.settledPage + 1] + } else { + currentItem + } dynamicColorState.animate( - fromSeedColor = Color(currentItem.seedColor!!), - toSeedColor = Color(targetItem.seedColor!!), + fromSeedColor = Color(fromItem.seedColor!!), + toSeedColor = Color(toItem.seedColor!!), progress = offset ) } From 16df3a103d45b680b11202102c3007da22fa1c47 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 06:35:36 +0530 Subject: [PATCH 13/21] Move `DynamicColorState` creation inside `DynamicContentTheme` --- .../kotlin/dev/sasikanth/rss/reader/app/App.kt | 11 +---------- .../groupselection/ui/GroupSelectionSheet.kt | 1 - .../rss/reader/home/ui/FeaturedSection.kt | 2 +- .../sasikanth/rss/reader/home/ui/HomeScreen.kt | 2 +- .../rss/reader/ui/DynamicContentTheme.kt | 18 ++++++++---------- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt index 4f9756acd..9478dea76 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt @@ -53,9 +53,6 @@ import dev.sasikanth.rss.reader.settings.ui.SettingsScreen import dev.sasikanth.rss.reader.share.LocalShareHandler import dev.sasikanth.rss.reader.share.ShareHandler import dev.sasikanth.rss.reader.ui.DynamicContentTheme -import dev.sasikanth.rss.reader.ui.darkAppColorScheme -import dev.sasikanth.rss.reader.ui.lightAppColorScheme -import dev.sasikanth.rss.reader.ui.rememberDynamicColorState import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.LocalWindowSizeClass import me.tatarka.inject.annotations.Assisted @@ -95,13 +92,7 @@ fun App( LaunchedEffect(useDarkTheme) { onThemeChange(useDarkTheme) } - val dynamicColorsState = - rememberDynamicColorState( - defaultLightAppColorScheme = lightAppColorScheme(), - defaultDarkAppColorScheme = darkAppColorScheme(), - ) - - DynamicContentTheme(state = dynamicColorsState, useDarkTheme = useDarkTheme) { + DynamicContentTheme(useDarkTheme = useDarkTheme) { ProvideStrings { Box { Children( diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt index 16a035900..3c40ff22c 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt @@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme 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 a5d11d558..7c5888bc6 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 @@ -145,7 +145,7 @@ internal fun FeaturedSection( currentItem } - dynamicColorState.animate( + dynamicColorState?.animate( fromSeedColor = Color(fromItem.seedColor!!), toSeedColor = Color(toItem.seedColor!!), progress = offset diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt index c971d9e01..c3e0c9438 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt @@ -142,7 +142,7 @@ internal fun HomeScreen( LaunchedEffect(featuredPosts) { if (featuredPosts.isNullOrEmpty()) { - dynamicColorState.reset() + dynamicColorState?.reset() } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt index da846b657..ff0a8437a 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -39,12 +39,13 @@ import dev.sasikanth.rss.reader.utils.inverse import kotlin.math.absoluteValue @Composable -internal fun DynamicContentTheme( - state: DynamicColorState? = null, - useDarkTheme: Boolean = false, - content: @Composable () -> Unit -) { - val dynamicColorState = state ?: LocalDynamicColorState.current +internal fun DynamicContentTheme(useDarkTheme: Boolean = false, content: @Composable () -> Unit) { + val dynamicColorState = + LocalDynamicColorState.current + ?: rememberDynamicColorState( + defaultLightAppColorScheme = lightAppColorScheme(), + defaultDarkAppColorScheme = darkAppColorScheme(), + ) val colorScheme = if (useDarkTheme) { dynamicColorState.darkAppColorScheme @@ -309,7 +310,4 @@ fun AppColorScheme.animate( ) } -internal val LocalDynamicColorState = - staticCompositionLocalOf { - throw NullPointerException("Please provide a dynamic color state") - } +internal val LocalDynamicColorState = staticCompositionLocalOf { null } From 6700e206a7bee02910d318735a1894cb1caf661c Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 07:34:34 +0530 Subject: [PATCH 14/21] Add extension to convert Compose color to hex string --- .../rss/reader/reader/ui/ReaderScreen.kt | 13 +++++-------- .../dev/sasikanth/rss/reader/utils/StringExt.kt | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt index 9261f836e..af7080023 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/reader/ui/ReaderScreen.kt @@ -43,7 +43,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.dp @@ -52,7 +51,6 @@ import com.multiplatform.webview.web.LoadingState import com.multiplatform.webview.web.WebView import com.multiplatform.webview.web.rememberWebViewNavigator import com.multiplatform.webview.web.rememberWebViewStateWithHTMLData -import dev.sasikanth.material.color.utilities.utils.StringUtils import dev.sasikanth.rss.reader.platform.LocalLinkHandler import dev.sasikanth.rss.reader.reader.ReaderEvent import dev.sasikanth.rss.reader.reader.ReaderHTMLColors @@ -73,6 +71,7 @@ import dev.sasikanth.rss.reader.share.LocalShareHandler import dev.sasikanth.rss.reader.ui.AppTheme import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.asJSString +import dev.sasikanth.rss.reader.utils.hexString import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -220,12 +219,10 @@ internal fun ReaderScreen( ) } - val codeBackgroundColor = - StringUtils.hexFromArgb(AppTheme.colorScheme.surfaceContainerHighest.toArgb()) - val textColor = StringUtils.hexFromArgb(AppTheme.colorScheme.onSurface.toArgb()) - val linkColor = StringUtils.hexFromArgb(AppTheme.colorScheme.tintedForeground.toArgb()) - val dividerColor = - StringUtils.hexFromArgb(AppTheme.colorScheme.surfaceContainerHigh.toArgb()) + val codeBackgroundColor = AppTheme.colorScheme.surfaceContainerHighest.hexString() + val textColor = AppTheme.colorScheme.onSurface.hexString() + val linkColor = AppTheme.colorScheme.tintedForeground.hexString() + val dividerColor = AppTheme.colorScheme.surfaceContainerHigh.hexString() val colors = ReaderHTMLColors( diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/StringExt.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/StringExt.kt index ce6092198..31b14f0d2 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/StringExt.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/StringExt.kt @@ -16,6 +16,10 @@ package dev.sasikanth.rss.reader.utils +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import dev.sasikanth.material.color.utilities.utils.ColorUtils +import korlibs.util.format import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -24,3 +28,16 @@ val String?.asJSString: String val data = Json.encodeToString(this.orEmpty()) return data } + +/** + * Hex string representing color, ex. #ff0000 for red. + * + * @param argb ARGB representation of a color. + */ +fun Color.hexString(): String { + val argb = toArgb() + val red: Int = ColorUtils.redFromArgb(argb) + val blue: Int = ColorUtils.blueFromArgb(argb) + val green: Int = ColorUtils.greenFromArgb(argb) + return "#%02x%02x%02x".format(red, green, blue) +} From 19e8aa2f1c65be4ae295e4ce54b574b67832d4cc Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 07:40:35 +0530 Subject: [PATCH 15/21] Remove `measureTimedValue` function usage from `HomePresenter` --- .../kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt index afa57740a..8a88865da 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt @@ -45,7 +45,6 @@ import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.NTuple4 import dev.sasikanth.rss.reader.utils.getLast24HourStart import dev.sasikanth.rss.reader.utils.getTodayStartInstant -import kotlin.time.measureTimedValue import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -325,13 +324,11 @@ class HomePresenter( .map { featuredPosts -> featuredPosts .map { postWithMetadata -> - val seedColor = measureTimedValue { - seedColorExtractor.calculateSeedColor(postWithMetadata.imageUrl) - } + val seedColor = seedColorExtractor.calculateSeedColor(postWithMetadata.imageUrl) FeaturedPostItem( postWithMetadata = postWithMetadata, - seedColor = seedColor.value, + seedColor = seedColor, ) } .toImmutableList() From fff16c438982ccf0983f9f757f72c853c6415191 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 07:41:09 +0530 Subject: [PATCH 16/21] Return `null` if image fails to load during seed color extraction --- .../dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt index f053d6a14..788667df0 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/SeedColorExtractor.kt @@ -25,7 +25,6 @@ import dev.sasikanth.material.color.utilities.quantize.QuantizerCelebi import dev.sasikanth.material.color.utilities.score.Score import dev.sasikanth.rss.reader.utils.toComposeImageBitmap import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException import kotlinx.coroutines.suspendCancellableCoroutine import me.tatarka.inject.annotations.Inject @@ -38,7 +37,7 @@ class SeedColorExtractor( suspend fun calculateSeedColor(url: String?): Int? { if (url.isNullOrBlank()) return null - val bitmap = suspendCancellableCoroutine { cont -> + val bitmap: ImageBitmap? = suspendCancellableCoroutine { cont -> val request = ImageRequest.Builder(platformContext.value) .data(url) @@ -47,14 +46,14 @@ class SeedColorExtractor( onSuccess = { result -> cont.resume(result.toComposeImageBitmap(platformContext.value)) }, - onError = { cont.resumeWithException(IllegalArgumentException()) }, + onError = { cont.resume(null) }, ) .build() imageLoader.value.enqueue(request) } - return bitmap.seedColor() + return bitmap?.seedColor() } private fun ImageBitmap.seedColor(): Int { From daca032b798ca8f6c14a42882018b11adf952730 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 08:33:14 +0530 Subject: [PATCH 17/21] Change selected dropdown menu item background color to `tintedHighlight` --- .../kotlin/dev/sasikanth/rss/reader/home/ui/HomeTopAppBar.kt | 2 +- .../dev/sasikanth/rss/reader/settings/ui/SettingsScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 a2ed34669..bd1d89d98 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 @@ -331,7 +331,7 @@ private fun PostsFilterDropdown( val label = getPostTypeLabel(type) val color = if (postsType == type) { - AppTheme.colorScheme.tintedSurface + AppTheme.colorScheme.tintedHighlight } else { Color.Unspecified } 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 f1d922132..92557cdb0 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 @@ -339,7 +339,7 @@ private fun PostsDeletionPeriodSettingItem( val backgroundColor = if (period == postsDeletionPeriod) { - AppTheme.colorScheme.tintedSurface + AppTheme.colorScheme.tintedHighlight } else { Color.Unspecified } From bfc6379f99ef54dd189ec4ea99a4c9d9305873bc Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 09:47:45 +0530 Subject: [PATCH 18/21] Remove unnecessary `MaterialTheme` usage in `SettingsScreen` If we update the global `MaterialTheme` it will recompose the entire tree, which is not what we want. --- .../rss/reader/settings/ui/SettingsScreen.kt | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) 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 92557cdb0..12b999655 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 @@ -46,7 +46,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -486,29 +485,19 @@ private fun BrowserTypeSettingItem( Spacer(Modifier.width(16.dp)) - MaterialTheme( - colorScheme = - darkColorScheme( - primary = AppTheme.colorScheme.tintedSurface, - onPrimary = AppTheme.colorScheme.tintedForeground, - outline = AppTheme.colorScheme.outline, - surfaceVariant = AppTheme.colorScheme.surfaceContainer - ) - ) { - Switch( - checked = checked, - onCheckedChange = { checked -> - val newBrowserType = - if (checked) { - BrowserType.InApp - } else { - BrowserType.Default - } + Switch( + checked = checked, + onCheckedChange = { checked -> + val newBrowserType = + if (checked) { + BrowserType.InApp + } else { + BrowserType.Default + } - onBrowserTypeChanged(newBrowserType) - }, - ) - } + onBrowserTypeChanged(newBrowserType) + }, + ) } } } From ec99d1a0092694a8e07351fff41c533346924fab Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 10:56:17 +0530 Subject: [PATCH 19/21] Change scope of dynamic color state usage when creating app theme --- .../dev/sasikanth/rss/reader/home/ui/HomeScreen.kt | 3 +-- .../kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt | 11 +++++++++-- .../sasikanth/rss/reader/ui/DynamicContentTheme.kt | 12 +++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt index c3e0c9438..d944b15f4 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/ui/HomeScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight @@ -154,7 +153,7 @@ internal fun HomeScreen( DynamicContentTheme(useDarkTheme = useDarkTheme) { Box( modifier = - Modifier.fillMaxWidth().background(AppTheme.colorScheme.surfaceContainerLowest) + Modifier.fillMaxSize().background(AppTheme.colorScheme.surfaceContainerLowest) ) { val posts = state.posts?.collectAsLazyPagingItems() val hasFeeds = state.hasFeeds diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt index 38223265a..a3bc88c14 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt @@ -34,16 +34,23 @@ import twine.shared.generated.resources.golos_regular @Composable internal fun AppTheme( + dynamicColorState: DynamicColorState, useDarkTheme: Boolean = false, - appColorScheme: AppColorScheme, content: @Composable () -> Unit ) { MaterialTheme( colorScheme = if (useDarkTheme) darkColorScheme() else lightColorScheme(), typography = typography(GolosFontFamily), ) { + val colorScheme = + if (useDarkTheme) { + dynamicColorState.darkAppColorScheme + } else { + dynamicColorState.lightAppColorScheme + } + CompositionLocalProvider( - LocalAppColorScheme provides appColorScheme, + LocalAppColorScheme provides colorScheme, LocalRippleTheme provides AppRippleTheme ) { content() diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt index ff0a8437a..5690fee9b 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -20,11 +20,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.toArgb @@ -46,15 +46,9 @@ internal fun DynamicContentTheme(useDarkTheme: Boolean = false, content: @Compos defaultLightAppColorScheme = lightAppColorScheme(), defaultDarkAppColorScheme = darkAppColorScheme(), ) - val colorScheme = - if (useDarkTheme) { - dynamicColorState.darkAppColorScheme - } else { - dynamicColorState.lightAppColorScheme - } CompositionLocalProvider(LocalDynamicColorState provides dynamicColorState) { - AppTheme(useDarkTheme = useDarkTheme, appColorScheme = colorScheme, content = content) + AppTheme(dynamicColorState = dynamicColorState, useDarkTheme = useDarkTheme, content = content) } } @@ -310,4 +304,4 @@ fun AppColorScheme.animate( ) } -internal val LocalDynamicColorState = staticCompositionLocalOf { null } +internal val LocalDynamicColorState = compositionLocalOf { null } From fff2d7904344e7e8cbe9df25aeddda14d3968981 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 11:14:52 +0530 Subject: [PATCH 20/21] Provide `DynamicContentTheme` using static composition local --- .../dev/sasikanth/rss/reader/app/App.kt | 14 ++++++++++++-- .../dev/sasikanth/rss/reader/ui/AppTheme.kt | 7 ++----- .../rss/reader/ui/DynamicContentTheme.kt | 19 ++++++------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt index 9478dea76..61511dbec 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt @@ -53,6 +53,10 @@ import dev.sasikanth.rss.reader.settings.ui.SettingsScreen import dev.sasikanth.rss.reader.share.LocalShareHandler import dev.sasikanth.rss.reader.share.ShareHandler import dev.sasikanth.rss.reader.ui.DynamicContentTheme +import dev.sasikanth.rss.reader.ui.LocalDynamicColorState +import dev.sasikanth.rss.reader.ui.darkAppColorScheme +import dev.sasikanth.rss.reader.ui.lightAppColorScheme +import dev.sasikanth.rss.reader.ui.rememberDynamicColorState import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.LocalWindowSizeClass import me.tatarka.inject.annotations.Assisted @@ -71,14 +75,20 @@ fun App( dispatchersProvider: DispatchersProvider, @Assisted onThemeChange: (useDarkTheme: Boolean) -> Unit ) { - val appState by appPresenter.state.collectAsState() - setSingletonImageLoaderFactory { imageLoader } + val appState by appPresenter.state.collectAsState() + val dynamicColorState = + rememberDynamicColorState( + defaultLightAppColorScheme = lightAppColorScheme(), + defaultDarkAppColorScheme = darkAppColorScheme(), + ) + CompositionLocalProvider( LocalWindowSizeClass provides calculateWindowSizeClass(), LocalShareHandler provides shareHandler, LocalLinkHandler provides linkHandler, + LocalDynamicColorState provides dynamicColorState, ) { val isSystemInDarkTheme = isSystemInDarkTheme() val useDarkTheme = diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt index a3bc88c14..b942566cc 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppTheme.kt @@ -33,15 +33,12 @@ import twine.shared.generated.resources.golos_medium import twine.shared.generated.resources.golos_regular @Composable -internal fun AppTheme( - dynamicColorState: DynamicColorState, - useDarkTheme: Boolean = false, - content: @Composable () -> Unit -) { +internal fun AppTheme(useDarkTheme: Boolean = false, content: @Composable () -> Unit) { MaterialTheme( colorScheme = if (useDarkTheme) darkColorScheme() else lightColorScheme(), typography = typography(GolosFontFamily), ) { + val dynamicColorState = LocalDynamicColorState.current val colorScheme = if (useDarkTheme) { dynamicColorState.darkAppColorScheme diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt index 5690fee9b..6bb06d844 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/DynamicContentTheme.kt @@ -17,14 +17,13 @@ package dev.sasikanth.rss.reader.ui import androidx.collection.lruCache import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.toArgb @@ -40,16 +39,7 @@ import kotlin.math.absoluteValue @Composable internal fun DynamicContentTheme(useDarkTheme: Boolean = false, content: @Composable () -> Unit) { - val dynamicColorState = - LocalDynamicColorState.current - ?: rememberDynamicColorState( - defaultLightAppColorScheme = lightAppColorScheme(), - defaultDarkAppColorScheme = darkAppColorScheme(), - ) - - CompositionLocalProvider(LocalDynamicColorState provides dynamicColorState) { - AppTheme(dynamicColorState = dynamicColorState, useDarkTheme = useDarkTheme, content = content) - } + AppTheme(useDarkTheme = useDarkTheme, content = content) } @Composable @@ -304,4 +294,7 @@ fun AppColorScheme.animate( ) } -internal val LocalDynamicColorState = compositionLocalOf { null } +internal val LocalDynamicColorState = + staticCompositionLocalOf { + throw NullPointerException("Please provide a dynamic color state") + } From 3f3ff41d094c1293af7eb709fe69b3db6f52256b Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Wed, 7 Aug 2024 11:22:10 +0530 Subject: [PATCH 21/21] Mark `AppColorScheme` as `Immutable` --- .../sasikanth/rss/reader/ui/AppColorScheme.kt | 86 ++++--------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt index acf67c14c..d700adca2 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/ui/AppColorScheme.kt @@ -15,82 +15,32 @@ */ package dev.sasikanth.rss.reader.ui -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.graphics.Color -@Stable +@Immutable class AppColorScheme( - tintedBackground: Color, - tintedSurface: Color, - tintedForeground: Color, - tintedHighlight: Color, - outline: Color, - outlineVariant: Color, - surface: Color, - onSurface: Color, - onSurfaceVariant: Color, - surfaceContainer: Color, - surfaceContainerLow: Color, - surfaceContainerLowest: Color, - surfaceContainerHigh: Color, - surfaceContainerHighest: Color, - textEmphasisHigh: Color, - textEmphasisMed: Color, + val tintedBackground: Color, + val tintedSurface: Color, + val tintedForeground: Color, + val tintedHighlight: Color, + val outline: Color, + val outlineVariant: Color, + val surface: Color, + val onSurface: Color, + val onSurfaceVariant: Color, + val surfaceContainer: Color, + val surfaceContainerLow: Color, + val surfaceContainerLowest: Color, + val surfaceContainerHigh: Color, + val surfaceContainerHighest: Color, + val textEmphasisHigh: Color, + val textEmphasisMed: Color, ) { - var tintedBackground by mutableStateOf(tintedBackground, structuralEqualityPolicy()) - internal set - - var tintedSurface by mutableStateOf(tintedSurface, structuralEqualityPolicy()) - internal set - - var tintedForeground by mutableStateOf(tintedForeground, structuralEqualityPolicy()) - internal set - - var tintedHighlight by mutableStateOf(tintedHighlight, structuralEqualityPolicy()) - internal set - - var outline by mutableStateOf(outline, structuralEqualityPolicy()) - internal set - - var outlineVariant by mutableStateOf(outlineVariant, structuralEqualityPolicy()) - internal set - - var surface by mutableStateOf(surface, structuralEqualityPolicy()) - internal set - - var onSurface by mutableStateOf(onSurface, structuralEqualityPolicy()) - internal set - - var onSurfaceVariant by mutableStateOf(onSurfaceVariant, structuralEqualityPolicy()) - internal set - - var surfaceContainer by mutableStateOf(surfaceContainer, structuralEqualityPolicy()) - internal set - - var surfaceContainerLow by mutableStateOf(surfaceContainerLow, structuralEqualityPolicy()) - internal set - - var surfaceContainerLowest by mutableStateOf(surfaceContainerLowest, structuralEqualityPolicy()) - internal set - - var surfaceContainerHigh by mutableStateOf(surfaceContainerHigh, structuralEqualityPolicy()) - internal set - - var surfaceContainerHighest by mutableStateOf(surfaceContainerHighest, structuralEqualityPolicy()) - internal set - - var textEmphasisHigh by mutableStateOf(textEmphasisHigh, structuralEqualityPolicy()) - internal set - - var textEmphasisMed by mutableStateOf(textEmphasisMed, structuralEqualityPolicy()) - internal set - fun copy( tintedBackground: Color = this.tintedBackground, tintedSurface: Color = this.surfaceContainer,