Skip to content

Commit 3ca77d2

Browse files
authored
Palette Color Scheme (#276)
1 parent ee5c5b5 commit 3ca77d2

File tree

38 files changed

+2254
-83
lines changed

38 files changed

+2254
-83
lines changed

androidApp/src/main/kotlin/org/michaelbel/movies/MainActivityContent.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,13 @@ internal fun MainActivityContent(
2828
viewModel: MainViewModel = koinViewModel(),
2929
enableEdgeToEdge: (Any, Any) -> Unit
3030
) {
31-
val currentTheme by viewModel.currentTheme.collectAsStateWithLifecycle()
32-
val dynamicColors by viewModel.dynamicColors.collectAsStateWithLifecycle()
31+
val themeData by viewModel.themeData.collectAsStateWithLifecycle()
3332
val navHostController = rememberNavController().apply {
3433
addOnDestinationChangedListener(viewModel::analyticsTrackDestination)
3534
}
3635

3736
MoviesTheme(
38-
theme = currentTheme,
39-
dynamicColors = dynamicColors,
37+
themeData = themeData,
4038
enableEdgeToEdge = enableEdgeToEdge
4139
) {
4240
NavHost(

androidApp/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import kotlinx.coroutines.flow.stateIn
1717
import kotlinx.coroutines.launch
1818
import org.michaelbel.movies.analytics.MoviesAnalytics
1919
import org.michaelbel.movies.app.BuildConfig
20+
import org.michaelbel.movies.common.ThemeData
2021
import org.michaelbel.movies.common.biometric.BiometricController
2122
import org.michaelbel.movies.common.biometric.BiometricListener
22-
import org.michaelbel.movies.common.theme.AppTheme
2323
import org.michaelbel.movies.common.viewmodel.BaseViewModel
2424
import org.michaelbel.movies.debug.notification.DebugNotificationClient
2525
import org.michaelbel.movies.interactor.Interactor
@@ -47,18 +47,11 @@ internal class MainViewModel(
4747
private val _splashLoading = MutableStateFlow(true)
4848
val splashLoading: StateFlow<Boolean> = _splashLoading.asStateFlow()
4949

50-
val currentTheme: StateFlow<AppTheme> = interactor.currentTheme
50+
val themeData: StateFlow<ThemeData> = interactor.themeData
5151
.stateIn(
5252
scope = this,
5353
started = SharingStarted.Lazily,
54-
initialValue = AppTheme.FollowSystem
55-
)
56-
57-
val dynamicColors: StateFlow<Boolean> = interactor.dynamicColors
58-
.stateIn(
59-
scope = this,
60-
started = SharingStarted.Lazily,
61-
initialValue = false
54+
initialValue = ThemeData.Default
6255
)
6356

6457
init {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.michaelbel.movies.common
2+
3+
import org.michaelbel.movies.common.theme.AppTheme
4+
5+
data class ThemeData(
6+
val appTheme: AppTheme,
7+
val dynamicColors: Boolean,
8+
val paletteKey: Int,
9+
val seedColor: Int
10+
) {
11+
companion object {
12+
const val STYLE_TONAL_SPOT = 0
13+
const val STYLE_SPRITZ = 1
14+
const val STYLE_FRUIT_SALAD = 2
15+
const val STYLE_VIBRANT = 3
16+
const val STYLE_MONOCHROME = 4
17+
18+
const val DEFAULT_SEED_COLOR = -12687058 // First color from Palette List
19+
const val DEFAULT_SEED_COLOR_HEX = 0xFF3E692E // String.format("#%06X", (0xFFFFFF and DEFAULT_SEED_COLOR))
20+
21+
val Default: ThemeData
22+
get() = ThemeData(
23+
appTheme = AppTheme.FollowSystem,
24+
dynamicColors = false,
25+
paletteKey = STYLE_TONAL_SPOT,
26+
seedColor = DEFAULT_SEED_COLOR
27+
)
28+
}
29+
}

core/debug-kmp/src/androidMain/kotlin/org/michaelbel/movies/debug/DebugViewModel.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,18 @@ package org.michaelbel.movies.debug
33
import kotlinx.coroutines.flow.SharingStarted
44
import kotlinx.coroutines.flow.StateFlow
55
import kotlinx.coroutines.flow.stateIn
6-
import org.michaelbel.movies.common.theme.AppTheme
6+
import org.michaelbel.movies.common.ThemeData
77
import org.michaelbel.movies.common.viewmodel.BaseViewModel
88
import org.michaelbel.movies.interactor.Interactor
99

1010
internal class DebugViewModel(
1111
interactor: Interactor
1212
): BaseViewModel() {
1313

14-
val currentTheme: StateFlow<AppTheme> = interactor.currentTheme
14+
val themeData: StateFlow<ThemeData> = interactor.themeData
1515
.stateIn(
1616
scope = this,
1717
started = SharingStarted.Lazily,
18-
initialValue = AppTheme.FollowSystem
19-
)
20-
21-
val dynamicColors: StateFlow<Boolean> = interactor.dynamicColors
22-
.stateIn(
23-
scope = this,
24-
started = SharingStarted.Lazily,
25-
initialValue = false
18+
initialValue = ThemeData.Default
2619
)
2720
}

core/debug-kmp/src/androidMain/kotlin/org/michaelbel/movies/debug/ui/DebugActivityContent.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ internal fun DebugActivityContent(
3939
viewModel: DebugViewModel = koinViewModel(),
4040
enableEdgeToEdge: (Any, Any) -> Unit
4141
) {
42-
val currentTheme by viewModel.currentTheme.collectAsStateWithLifecycle()
43-
val dynamicColors by viewModel.dynamicColors.collectAsStateWithLifecycle()
42+
val themeData by viewModel.themeData.collectAsStateWithLifecycle()
4443

4544
val context = LocalContext.current
4645
val resultContract = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
@@ -51,8 +50,7 @@ internal fun DebugActivityContent(
5150
)
5251

5352
MoviesTheme(
54-
theme = currentTheme,
55-
dynamicColors = dynamicColors,
53+
themeData = themeData,
5654
enableEdgeToEdge = enableEdgeToEdge
5755
) {
5856
Scaffold(

core/interactor-kmp/src/commonMain/kotlin/org/michaelbel/movies/interactor/SettingsInteractor.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.michaelbel.movies.interactor
22

33
import kotlinx.coroutines.flow.Flow
4+
import org.michaelbel.movies.common.ThemeData
45
import org.michaelbel.movies.common.appearance.FeedView
56
import org.michaelbel.movies.common.list.MovieList
67
import org.michaelbel.movies.common.theme.AppTheme
@@ -13,7 +14,7 @@ interface SettingsInteractor {
1314

1415
val currentMovieList: Flow<MovieList>
1516

16-
val dynamicColors: Flow<Boolean>
17+
val themeData: Flow<ThemeData>
1718

1819
val isBiometricEnabled: Flow<Boolean>
1920

@@ -35,6 +36,14 @@ interface SettingsInteractor {
3536
value: Boolean
3637
)
3738

39+
suspend fun setPaletteKey(
40+
paletteKey: Int
41+
)
42+
43+
suspend fun setSeedColor(
44+
seedColor: Int
45+
)
46+
3847
suspend fun setBiometricEnabled(
3948
enabled: Boolean
4049
)

core/interactor-kmp/src/commonMain/kotlin/org/michaelbel/movies/interactor/impl/SettingsInteractorImpl.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.michaelbel.movies.analytics.event.ChangeDynamicColorsEvent
77
import org.michaelbel.movies.analytics.event.SelectFeedViewEvent
88
import org.michaelbel.movies.analytics.event.SelectMovieListEvent
99
import org.michaelbel.movies.analytics.event.SelectThemeEvent
10+
import org.michaelbel.movies.common.ThemeData
1011
import org.michaelbel.movies.common.appearance.FeedView
1112
import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
1213
import org.michaelbel.movies.common.list.MovieList
@@ -26,7 +27,7 @@ internal class SettingsInteractorImpl(
2627

2728
override val currentMovieList: Flow<MovieList> = settingsRepository.currentMovieList
2829

29-
override val dynamicColors: Flow<Boolean> = settingsRepository.dynamicColors
30+
override val themeData: Flow<ThemeData> = settingsRepository.themeData
3031

3132
override val isBiometricEnabled: Flow<Boolean> = settingsRepository.isBiometricEnabled
3233

@@ -62,6 +63,18 @@ internal class SettingsInteractorImpl(
6263
}
6364
}
6465

66+
override suspend fun setPaletteKey(paletteKey: Int) {
67+
withContext(dispatchers.main) {
68+
settingsRepository.setPaletteKey(paletteKey)
69+
}
70+
}
71+
72+
override suspend fun setSeedColor(seedColor: Int) {
73+
withContext(dispatchers.main) {
74+
settingsRepository.setSeedColor(seedColor)
75+
}
76+
}
77+
6578
override suspend fun setBiometricEnabled(enabled: Boolean) {
6679
withContext(dispatchers.main) {
6780
settingsRepository.setBiometricEnabled(enabled)

core/persistence-kmp/src/commonMain/kotlin/org/michaelbel/movies/persistence/datastore/MoviesPreferences.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ class MoviesPreferences(
2727
val isDynamicColorsFlow: Flow<Boolean?>
2828
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_DYNAMIC_COLORS_KEY] }
2929

30-
val isBiometricEnabled: Flow<Boolean?>
30+
val isBiometricEnabledFlow: Flow<Boolean?>
3131
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_BIOMETRIC_KEY] }
3232

33+
val paletteKeyFlow: Flow<Int?>
34+
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_PALETTE_KEY] }
35+
36+
val seedColorFlow: Flow<Int?>
37+
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_SEED_COLOR_KEY] }
38+
3339
val accountIdFlow: Flow<Int?>
3440
get() = dataStore.data.map { preferences -> preferences[PREFERENCE_ACCOUNT_ID_KEY] }
3541

@@ -119,6 +125,18 @@ class MoviesPreferences(
119125
}
120126
}
121127

128+
suspend fun setPaletteKey(paletteKey: Int) {
129+
dataStore.edit { preferences ->
130+
preferences[PREFERENCE_PALETTE_KEY] = paletteKey
131+
}
132+
}
133+
134+
suspend fun setSeedColor(seedColor: Int) {
135+
dataStore.edit { preferences ->
136+
preferences[PREFERENCE_SEED_COLOR_KEY] = seedColor
137+
}
138+
}
139+
122140
private companion object {
123141
private val PREFERENCE_THEME_KEY = stringPreferencesKey("theme")
124142
private val PREFERENCE_FEED_VIEW_KEY = stringPreferencesKey("feed_view")
@@ -129,5 +147,7 @@ class MoviesPreferences(
129147
private val PREFERENCE_ACCOUNT_EXPIRE_TIME_KEY = longPreferencesKey("account_expire_time")
130148
private val PREFERENCE_NOTIFICATION_EXPIRE_TIME_KEY = longPreferencesKey("notification_expire_time")
131149
private val PREFERENCE_BIOMETRIC_KEY = booleanPreferencesKey("biometric")
150+
private val PREFERENCE_PALETTE_KEY = intPreferencesKey("palette")
151+
private val PREFERENCE_SEED_COLOR_KEY = intPreferencesKey("seed_color")
132152
}
133153
}

core/repository-kmp/src/commonMain/kotlin/org/michaelbel/movies/repository/SettingsRepository.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.michaelbel.movies.repository
22

33
import kotlinx.coroutines.flow.Flow
4+
import org.michaelbel.movies.common.ThemeData
45
import org.michaelbel.movies.common.appearance.FeedView
56
import org.michaelbel.movies.common.list.MovieList
67
import org.michaelbel.movies.common.theme.AppTheme
@@ -13,7 +14,7 @@ interface SettingsRepository {
1314

1415
val currentMovieList: Flow<MovieList>
1516

16-
val dynamicColors: Flow<Boolean>
17+
val themeData: Flow<ThemeData>
1718

1819
val isBiometricEnabled: Flow<Boolean>
1920

@@ -35,6 +36,14 @@ interface SettingsRepository {
3536
value: Boolean
3637
)
3738

39+
suspend fun setPaletteKey(
40+
paletteKey: Int
41+
)
42+
43+
suspend fun setSeedColor(
44+
seedColor: Int
45+
)
46+
3847
suspend fun setBiometricEnabled(
3948
enabled: Boolean
4049
)

core/repository-kmp/src/commonMain/kotlin/org/michaelbel/movies/repository/impl/SettingsRepositoryImpl.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.michaelbel.movies.repository.impl
22

33
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.combine
45
import kotlinx.coroutines.flow.map
6+
import org.michaelbel.movies.common.ThemeData
57
import org.michaelbel.movies.common.appearance.FeedView
68
import org.michaelbel.movies.common.list.MovieList
79
import org.michaelbel.movies.common.theme.AppTheme
@@ -25,11 +27,24 @@ internal class SettingsRepositoryImpl(
2527
MovieList.transform(className ?: MovieList.NowPlaying().toString())
2628
}
2729

28-
override val dynamicColors: Flow<Boolean> = preferences.isDynamicColorsFlow.map { isDynamicColors ->
29-
isDynamicColors ?: defaultDynamicColorsEnabled
30-
}
30+
override val themeData: Flow<ThemeData>
31+
get() {
32+
return combine(
33+
preferences.themeFlow,
34+
preferences.isDynamicColorsFlow,
35+
preferences.paletteKeyFlow,
36+
preferences.seedColorFlow
37+
) { name, dynamicColors, paletteKey, seedColor ->
38+
ThemeData(
39+
appTheme = AppTheme.transform(name ?: AppTheme.FollowSystem.toString()),
40+
dynamicColors = dynamicColors ?: defaultDynamicColorsEnabled,
41+
paletteKey = paletteKey ?: ThemeData.STYLE_TONAL_SPOT,
42+
seedColor = seedColor ?: ThemeData.DEFAULT_SEED_COLOR
43+
)
44+
}
45+
}
3146

32-
override val isBiometricEnabled: Flow<Boolean> = preferences.isBiometricEnabled.map { enabled ->
47+
override val isBiometricEnabled: Flow<Boolean> = preferences.isBiometricEnabledFlow.map { enabled ->
3348
enabled ?: false
3449
}
3550

@@ -53,6 +68,14 @@ internal class SettingsRepositoryImpl(
5368
preferences.setDynamicColors(value)
5469
}
5570

71+
override suspend fun setPaletteKey(paletteKey: Int) {
72+
preferences.setPaletteKey(paletteKey)
73+
}
74+
75+
override suspend fun setSeedColor(seedColor: Int) {
76+
preferences.setSeedColor(seedColor)
77+
}
78+
5679
override suspend fun setBiometricEnabled(enabled: Boolean) {
5780
preferences.setBiometricEnabled(enabled)
5881
}

0 commit comments

Comments
 (0)