Skip to content

Commit bb30156

Browse files
committed
bugfix: react to changed theme settings
1 parent 1b9ffbe commit bb30156

9 files changed

Lines changed: 126 additions & 45 deletions

File tree

app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainActivity.kt

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.androidvip.sysctlgui.ui.main
22

33
import android.app.NotificationManager
4+
import android.content.res.Configuration
45
import android.graphics.Color
56
import android.os.Build
67
import android.os.Bundle
@@ -11,7 +12,10 @@ import androidx.activity.compose.setContent
1112
import androidx.activity.enableEdgeToEdge
1213
import androidx.activity.result.contract.ActivityResultContracts
1314
import androidx.compose.foundation.isSystemInDarkTheme
15+
import androidx.compose.runtime.LaunchedEffect
16+
import androidx.compose.runtime.getValue
1417
import androidx.core.os.postDelayed
18+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1519
import com.androidvip.sysctlgui.core.navigation.UiRoute
1620
import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
1721
import com.androidvip.sysctlgui.domain.enums.Actions
@@ -20,31 +24,26 @@ import org.koin.android.ext.android.inject
2024

2125
class MainActivity : ComponentActivity() {
2226
private val prefs: AppPrefs by inject()
27+
private val mainViewModel: MainViewModel by inject()
2328
private val notificationPermissionLauncher =
2429
registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ -> }
2530

2631
override fun onCreate(savedInstanceState: Bundle?) {
2732
super.onCreate(savedInstanceState)
28-
enableEdgeToEdge(
29-
statusBarStyle = SystemBarStyle.auto(
30-
lightScrim = Color.TRANSPARENT,
31-
darkScrim = Color.TRANSPARENT,
32-
detectDarkMode = { resources ->
33-
prefs.forceDark ||
34-
resources.configuration.uiMode and
35-
android.content.res.Configuration.UI_MODE_NIGHT_MASK ==
36-
android.content.res.Configuration.UI_MODE_NIGHT_YES
37-
}
38-
)
39-
)
33+
updateEdgeToEdgeConfiguration(prefs.forceDark)
4034

4135
setContent {
42-
// TODO: Make the switch dynamic with the view model
43-
// TODO: Translations
36+
val themeState by mainViewModel.themeState.collectAsStateWithLifecycle()
37+
val forceDark = themeState.forceDark
38+
39+
LaunchedEffect(forceDark) {
40+
updateEdgeToEdgeConfiguration(forceDark)
41+
}
42+
4443
SysctlGuiTheme(
45-
darkTheme = prefs.forceDark || isSystemInDarkTheme(),
46-
contrastLevel = prefs.contrastLevel,
47-
dynamicColor = prefs.dynamicColors
44+
darkTheme = forceDark || isSystemInDarkTheme(),
45+
contrastLevel = themeState.contrastLevel,
46+
dynamicColor = themeState.dynamicColors
4847
) {
4948
MainScreen(startDestination = getRouteFromIntent())
5049
}
@@ -55,6 +54,20 @@ class MainActivity : ComponentActivity() {
5554
}
5655
}
5756

57+
private fun updateEdgeToEdgeConfiguration(forceDark: Boolean) {
58+
enableEdgeToEdge(
59+
statusBarStyle = SystemBarStyle.auto(
60+
lightScrim = Color.TRANSPARENT,
61+
darkScrim = Color.TRANSPARENT,
62+
detectDarkMode = { resources ->
63+
val isSystemDark = resources.configuration.uiMode and
64+
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
65+
forceDark || isSystemDark
66+
}
67+
)
68+
)
69+
}
70+
5871
private fun getRouteFromIntent(): UiRoute {
5972
val extraDestination = intent.getStringExtra(EXTRA_DESTINATION)
6073
?: return UiRoute.BrowseParams

app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewModel.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,39 @@
11
package com.androidvip.sysctlgui.ui.main
22

33
import androidx.compose.material3.SnackbarResult
4+
import androidx.lifecycle.viewModelScope
5+
import com.androidvip.sysctlgui.data.Prefs
6+
import com.androidvip.sysctlgui.data.repository.CONTRAST_LEVEL_NORMAL
7+
import com.androidvip.sysctlgui.domain.repository.AppPrefs
48
import com.androidvip.sysctlgui.utils.BaseViewModel
9+
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.coroutines.flow.SharingStarted
11+
import kotlinx.coroutines.flow.combine
12+
import kotlinx.coroutines.flow.stateIn
513

6-
class MainViewModel : BaseViewModel<MainViewEvent, MainViewState, MainViewEffect>() {
14+
class MainViewModel(
15+
private val appPrefs: AppPrefs
16+
) : BaseViewModel<MainViewEvent, MainViewState, MainViewEffect>() {
17+
private val themeSettingsFlow: Flow<ThemeSettings> = combine(
18+
appPrefs.observeKey(Prefs.ForceDarkTheme.key, false),
19+
appPrefs.observeKey(Prefs.DynamicColors.key, false),
20+
appPrefs.observeKey(Prefs.ContrastLevel.key, CONTRAST_LEVEL_NORMAL)
21+
) { forceDark, dynamicColors, contrastLevel ->
22+
ThemeSettings(forceDark, dynamicColors, contrastLevel)
23+
}
24+
val themeState = themeSettingsFlow.stateIn(
25+
scope = viewModelScope,
26+
started = SharingStarted.WhileSubscribed(5000),
27+
initialValue = loadInitialThemeState()
28+
)
29+
30+
private fun loadInitialThemeState(): ThemeSettings {
31+
return ThemeSettings(
32+
forceDark = appPrefs.forceDark,
33+
contrastLevel = appPrefs.contrastLevel,
34+
dynamicColors = appPrefs.dynamicColors
35+
)
36+
}
737

838
override fun createInitialState() = MainViewState()
939

@@ -12,9 +42,11 @@ class MainViewModel : BaseViewModel<MainViewEvent, MainViewState, MainViewEffect
1242
is MainViewEvent.OnSateChangeRequested -> {
1343
setState { event.newState }
1444
}
45+
1546
is MainViewEvent.ShowSnackbarRequested -> {
1647
setEffect { MainViewEffect.ShowSnackbar(event.message, event.actionLabel) }
1748
}
49+
1850
is MainViewEvent.OnSnackbarResult -> {
1951
val snackbarResult = event.result
2052
if (snackbarResult == SnackbarResult.ActionPerformed) {

app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.androidvip.sysctlgui.ui.main
22

33
import androidx.compose.material3.SnackbarResult
4+
import com.androidvip.sysctlgui.data.repository.CONTRAST_LEVEL_NORMAL
45

56
data class MainViewState(
67
val topBarTitle: String = "SysctlGUI",
@@ -10,6 +11,12 @@ data class MainViewState(
1011
val showSearchAction: Boolean = true
1112
)
1213

14+
data class ThemeSettings(
15+
val forceDark: Boolean = false,
16+
val dynamicColors: Boolean = false,
17+
val contrastLevel: Int = CONTRAST_LEVEL_NORMAL
18+
)
19+
1320
sealed interface MainViewEffect {
1421
data class ShowSnackbar(val message: String, val actionLabel: String? = null) : MainViewEffect
1522
data object ActUponSckbarActionPerformed : MainViewEffect

app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsScreen.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ internal fun SettingsScreen(
7070
}
7171
)
7272

73-
7473
LaunchedEffect(Unit) {
7574
mainViewModel.onEvent(
7675
MainViewEvent.OnSateChangeRequested(

app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsViewModel.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ class SettingsViewModel(
1919

2020
init {
2121
viewModelScope.launch {
22-
val settings = getSettings()
23-
setState { copy(settings = settings) }
22+
loadSettings()
2423
}
2524
}
2625

@@ -62,7 +61,16 @@ class SettingsViewModel(
6261
if (event.appSetting.key == Prefs.RunOnStartup.key) {
6362
setEffect { SettingsViewEffect.RequestNotificationPermission }
6463
}
64+
65+
viewModelScope.launch {
66+
loadSettings()
67+
}
6568
}
6669
}
6770
}
68-
}
71+
72+
private suspend fun loadSettings() {
73+
val settings = getSettings()
74+
setState { copy(settings = settings) }
75+
}
76+
}

app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/TextSettingComponent.kt

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
2121
import androidx.compose.ui.Alignment
2222
import androidx.compose.ui.Modifier
2323
import androidx.compose.ui.tooling.preview.PreviewLightDark
24+
import androidx.compose.ui.unit.DpOffset
2425
import androidx.compose.ui.unit.dp
2526
import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
2627
import com.androidvip.sysctlgui.domain.enums.SettingItemType
@@ -46,7 +47,6 @@ fun TextSettingComponent(
4647
verticalAlignment = Alignment.CenterVertically,
4748
horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
4849
) {
49-
5050
Box(
5151
modifier = Modifier
5252
.align(Alignment.CenterVertically)
@@ -62,27 +62,27 @@ fun TextSettingComponent(
6262
modifier = Modifier.fillMaxWidth()
6363
)
6464
}
65-
}
6665

67-
68-
DropdownMenu(
69-
expanded = expanded,
70-
onDismissRequest = { expanded = false }
71-
) {
72-
appSetting.values?.forEach { item ->
73-
DropdownMenuItem(
74-
text = {
75-
Text(
76-
text = item as String,
77-
style = MaterialTheme.typography.bodyMedium,
78-
color = MaterialTheme.colorScheme.onSurface
79-
)
80-
},
81-
onClick = {
82-
onValueChange(item as String)
83-
expanded = false
84-
}
85-
)
66+
DropdownMenu(
67+
expanded = expanded,
68+
offset = DpOffset(16.dp, (-32).dp),
69+
onDismissRequest = { expanded = false }
70+
) {
71+
appSetting.values?.forEach { item ->
72+
DropdownMenuItem(
73+
text = {
74+
Text(
75+
text = item as String,
76+
style = MaterialTheme.typography.bodyMedium,
77+
color = MaterialTheme.colorScheme.onSurface
78+
)
79+
},
80+
onClick = {
81+
onValueChange(item as String)
82+
expanded = false
83+
}
84+
)
85+
}
8686
}
8787
}
8888
}

common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Theme.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package com.androidvip.sysctlgui.design.theme
22

33
import android.os.Build
44
import androidx.compose.foundation.isSystemInDarkTheme
5-
import androidx.compose.material3.MaterialTheme
5+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
6+
import androidx.compose.material3.MaterialExpressiveTheme
67
import androidx.compose.material3.darkColorScheme
78
import androidx.compose.material3.dynamicDarkColorScheme
89
import androidx.compose.material3.dynamicLightColorScheme
@@ -238,6 +239,7 @@ private val highContrastDarkColorScheme = darkColorScheme(
238239
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
239240
)
240241

242+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
241243
@Composable
242244
fun SysctlGuiTheme(
243245
darkTheme: Boolean = isSystemInDarkTheme(),
@@ -264,7 +266,7 @@ fun SysctlGuiTheme(
264266
}
265267
}
266268

267-
MaterialTheme(
269+
MaterialExpressiveTheme(
268270
colorScheme = colorScheme,
269271
typography = Typography,
270272
content = content

data/src/main/java/com/androidvip/sysctlgui/data/repository/AppPrefsImpl.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@ import android.content.SharedPreferences
44
import androidx.core.content.edit
55
import com.androidvip.sysctlgui.data.Prefs
66
import com.androidvip.sysctlgui.domain.repository.AppPrefs
7+
import kotlinx.coroutines.channels.awaitClose
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.callbackFlow
710

811
/**
912
* Implementation of [AppPrefs] that uses [SharedPreferences] to store and retrieve app preferences.
1013
*/
1114
class AppPrefsImpl(private val prefs: SharedPreferences) : AppPrefs {
15+
@Suppress("UNCHECKED_CAST")
16+
override fun <T> observeKey(key: String, default: T): Flow<T> = callbackFlow {
17+
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, updatedKey ->
18+
if (updatedKey == key) {
19+
trySend(prefs.all[key] as T ?: default)
20+
}
21+
}
22+
prefs.registerOnSharedPreferenceChangeListener(listener)
23+
trySend(prefs.all[key] as T ?: default)
24+
awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }
25+
}
26+
1227
override var listFoldersFirst: Boolean
1328
get() = prefs.getBoolean(Prefs.ListFoldersFirst.key, true)
1429
set(value) {
@@ -88,4 +103,6 @@ class AppPrefsImpl(private val prefs: SharedPreferences) : AppPrefs {
88103
currentHistory.remove(query)
89104
prefs.edit { putStringSet(Prefs.SearchHistory.key, currentHistory) }
90105
}
106+
107+
91108
}

domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppPrefs.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.androidvip.sysctlgui.domain.repository
22

3+
import kotlinx.coroutines.flow.Flow
4+
35
/**
46
* Interface for accessing and modifying application preferences.
57
*
68
* This interface defines the contract for interacting with the application's settings,
79
* allowing various parts of the app to read and write preference values.
810
*/
911
interface AppPrefs {
12+
fun <T> observeKey(key: String, default: T): Flow<T>
1013
var listFoldersFirst: Boolean
1114
var guessInputType: Boolean
1215
var commitMode: String

0 commit comments

Comments
 (0)