Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dynamic light mode #692

Merged
merged 21 commits into from
Aug 7, 2024
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
33ef70f
Add class to extract seed color from a image URL
msasikanth Aug 6, 2024
3f5bc72
Enable automatic UI user interface style for iOS
msasikanth Aug 6, 2024
71b9cfd
Add support for storing app theme mode in preferences
msasikanth Aug 6, 2024
a911c31
Change seed color extraction to return color int
msasikanth Aug 6, 2024
dcdb259
Add default light color scheme
msasikanth Aug 6, 2024
55d882d
Use color scheme tokens in `PostMetadata`
msasikanth Aug 6, 2024
71198db
Hide `FeaturedSection` if there are no featured posts
msasikanth Aug 6, 2024
0866999
Add support for dynamic light theme
msasikanth Aug 6, 2024
2476cc3
Use LRU cache for storing dynamic colors during animation
msasikanth Aug 7, 2024
a45b939
Remove unused code in `DynamicContentTheme`
msasikanth Aug 7, 2024
23dbce7
Reduce cache size in `DynamicContentTheme`
msasikanth Aug 7, 2024
337dde7
Use pager offset for calculating from and to seed colors in `Featured…
msasikanth Aug 7, 2024
16df3a1
Move `DynamicColorState` creation inside `DynamicContentTheme`
msasikanth Aug 7, 2024
6700e20
Add extension to convert Compose color to hex string
msasikanth Aug 7, 2024
19e8aa2
Remove `measureTimedValue` function usage from `HomePresenter`
msasikanth Aug 7, 2024
fff16c4
Return `null` if image fails to load during seed color extraction
msasikanth Aug 7, 2024
daca032
Change selected dropdown menu item background color to `tintedHighlight`
msasikanth Aug 7, 2024
bfc6379
Remove unnecessary `MaterialTheme` usage in `SettingsScreen`
msasikanth Aug 7, 2024
ec99d1a
Change scope of dynamic color state usage when creating app theme
msasikanth Aug 7, 2024
fff2d79
Provide `DynamicContentTheme` using static composition local
msasikanth Aug 7, 2024
3f3ff41
Mark `AppColorScheme` as `Immutable`
msasikanth Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
}
}

Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ class SettingsRepository(private val dataStore: DataStore<Preferences>) {
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<BrowserType> =
dataStore.data.map { preferences ->
@@ -73,6 +74,11 @@ class SettingsRepository(private val dataStore: DataStore<Preferences>) {
mapToFeedsOrderBy(preferences[feedsSortOrderKey]) ?: FeedsOrderBy.Latest
}

val appThemeMode: Flow<AppThemeMode> =
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<Preferences>) {
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<Preferences>) {
}
}

enum class AppThemeMode {
Light,
Dark,
Auto
}

enum class BrowserType {
Default,
InApp
2 changes: 1 addition & 1 deletion iosApp/iosApp/Info.plist
Original file line number Diff line number Diff line change
@@ -68,6 +68,6 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<string>Automatic</string>
</dict>
</plist>
67 changes: 56 additions & 11 deletions shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/App.kt
Original file line number Diff line number Diff line change
@@ -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,17 @@ 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.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
import me.tatarka.inject.annotations.Inject

typealias App = @Composable () -> Unit
typealias App = @Composable (onThemeChange: (useDarkTheme: Boolean) -> Unit) -> Unit

@Inject
@Composable
@@ -64,18 +73,36 @@ fun App(
linkHandler: LinkHandler,
imageLoader: ImageLoader,
dispatchersProvider: DispatchersProvider,
@Assisted onThemeChange: (useDarkTheme: Boolean) -> Unit
) {
setSingletonImageLoaderFactory { imageLoader }

val dynamicColorState = rememberDynamicColorState(imageLoader = imageLoader)
val appState by appPresenter.state.collectAsState()
val dynamicColorState =
rememberDynamicColorState(
defaultLightAppColorScheme = lightAppColorScheme(),
defaultDarkAppColorScheme = darkAppColorScheme(),
)

CompositionLocalProvider(
LocalWindowSizeClass provides calculateWindowSizeClass(),
LocalDynamicColorState provides dynamicColorState,
LocalShareHandler provides shareHandler,
LocalLinkHandler provides linkHandler
LocalLinkHandler provides linkHandler,
LocalDynamicColorState provides dynamicColorState,
) {
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) }

DynamicContentTheme(useDarkTheme = useDarkTheme) {
ProvideStrings {
Box {
Children(
@@ -93,7 +120,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 +159,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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AppState> =
_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 {
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading