diff --git a/app/src/main/java/com/markreader/MainActivity.kt b/app/src/main/java/com/markreader/MainActivity.kt index 75f281a..8b0996b 100644 --- a/app/src/main/java/com/markreader/MainActivity.kt +++ b/app/src/main/java/com/markreader/MainActivity.kt @@ -37,7 +37,10 @@ class MainActivity : ComponentActivity() { .getInstance(applicationContext) .preferences .collectAsStateWithLifecycle(initialValue = UserPreferences()) - MarkReaderTheme(theme = preferences.appThemeMode.toAppTheme()) { + MarkReaderTheme( + theme = preferences.appThemeMode.toAppTheme(), + dynamicColor = preferences.useDynamicColors + ) { MarkReaderApp( externalUri = externalUri, externalUriNonce = externalUriNonce, diff --git a/app/src/main/java/com/markreader/data/PreferencesRepository.kt b/app/src/main/java/com/markreader/data/PreferencesRepository.kt index 339948c..497ef8a 100644 --- a/app/src/main/java/com/markreader/data/PreferencesRepository.kt +++ b/app/src/main/java/com/markreader/data/PreferencesRepository.kt @@ -3,6 +3,7 @@ package com.markreader.data import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey @@ -22,6 +23,7 @@ class PreferencesRepository private constructor(private val context: Context) { private object Keys { val LEGACY_THEME = stringPreferencesKey("theme") val APP_THEME_MODE = stringPreferencesKey("app_theme_mode") + val USE_DYNAMIC_COLORS = booleanPreferencesKey("use_dynamic_colors") val READER_LIGHT_THEME = stringPreferencesKey("reader_light_theme") val READER_DARK_THEME = stringPreferencesKey("reader_dark_theme") val READING_FONT = stringPreferencesKey("reading_font") @@ -74,6 +76,7 @@ class PreferencesRepository private constructor(private val context: Context) { UserPreferences( appThemeMode = appThemeMode, + useDynamicColors = prefs[Keys.USE_DYNAMIC_COLORS] ?: true, readerLightTheme = readerLightTheme, readerDarkTheme = readerDarkTheme, readingFont = readingFont, @@ -88,6 +91,10 @@ class PreferencesRepository private constructor(private val context: Context) { context.dataStore.edit { it[Keys.APP_THEME_MODE] = themeMode.name } } + suspend fun setUseDynamicColors(enabled: Boolean) { + context.dataStore.edit { it[Keys.USE_DYNAMIC_COLORS] = enabled } + } + suspend fun setReaderLightTheme(theme: ReaderThemePreference) { val safeTheme = if (theme == ReaderThemePreference.Sepia) ReaderThemePreference.Sepia else ReaderThemePreference.Light context.dataStore.edit { it[Keys.READER_LIGHT_THEME] = safeTheme.name } diff --git a/app/src/main/java/com/markreader/data/UserPreferences.kt b/app/src/main/java/com/markreader/data/UserPreferences.kt index 8a04506..08da062 100644 --- a/app/src/main/java/com/markreader/data/UserPreferences.kt +++ b/app/src/main/java/com/markreader/data/UserPreferences.kt @@ -30,6 +30,7 @@ enum class TextAlignmentPreference { data class UserPreferences( val appThemeMode: AppThemeModePreference = AppThemeModePreference.System, + val useDynamicColors: Boolean = true, val readerLightTheme: ReaderThemePreference = ReaderThemePreference.Light, val readerDarkTheme: ReaderThemePreference = ReaderThemePreference.Dark, val readingFont: ReadingFontPreference = ReadingFontPreference.Merriweather, diff --git a/app/src/main/java/com/markreader/ui/screens/SettingsScreen.kt b/app/src/main/java/com/markreader/ui/screens/SettingsScreen.kt index b84d6db..5a79e9b 100644 --- a/app/src/main/java/com/markreader/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/markreader/ui/screens/SettingsScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.automirrored.rounded.MenuBook import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.BrightnessAuto import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.FormatAlignJustify @@ -41,6 +42,7 @@ import androidx.compose.material.icons.rounded.FormatSize import androidx.compose.material.icons.rounded.LightMode import androidx.compose.material.icons.rounded.Palette import androidx.compose.material.icons.rounded.TextFields +import androidx.compose.material.icons.rounded.Wallpaper import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -54,6 +56,8 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.dynamicDarkColorScheme @@ -321,6 +325,63 @@ private fun OptionSheet( } } +@Composable +private fun SwitchSettingsRow( + position: SegmentPosition, + icon: ImageVector, + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + val haptics = LocalHapticFeedback.current + val toggle: (Boolean) -> Unit = { newValue -> + haptics.performHapticFeedback( + if (newValue) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff + ) + onCheckedChange(newValue) + } + + SettingsSurface(position = position, onClick = { toggle(!checked) }) { + Row(verticalAlignment = Alignment.CenterVertically) { + RowLeadingIcon(icon) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.width(12.dp)) + Switch( + checked = checked, + onCheckedChange = toggle, + thumbContent = { + AnimatedContent( + targetState = checked, + transitionSpec = { + fadeIn(tween(100)) togetherWith fadeOut(tween(100)) + }, + label = "switchThumbIcon" + ) { isChecked -> + Icon( + imageVector = if (isChecked) Icons.Rounded.Check else Icons.Rounded.Close, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize) + ) + } + } + ) + } + } +} + @Composable private fun SliderSettingsRow( position: SegmentPosition, @@ -419,11 +480,19 @@ private fun ReaderPreviewCard( val isSystemDark = isSystemInDarkTheme() val haptics = LocalHapticFeedback.current - val dynamicLightScheme = remember(context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicLightColorScheme(context) else null + val dynamicLightScheme = remember(context, preferences.useDynamicColors) { + if (preferences.useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + dynamicLightColorScheme(context) + } else { + null + } } - val dynamicDarkScheme = remember(context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicDarkColorScheme(context) else null + val dynamicDarkScheme = remember(context, preferences.useDynamicColors) { + if (preferences.useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + dynamicDarkColorScheme(context) + } else { + null + } } val (lightReaderColors, darkReaderColors) = resolveReaderColors( readerLightTheme = preferences.readerLightTheme, @@ -590,9 +659,10 @@ fun SettingsScreen( // ── Appearance ───────────────────────────────────────────── Column { SectionHeader("Appearance") + val supportsDynamicColors = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S SettingsGroup { SegmentedSettingsRow( - position = SegmentPosition.Single, + position = if (supportsDynamicColors) SegmentPosition.First else SegmentPosition.Single, icon = Icons.Rounded.Palette, title = "App theme", subtitle = "Overall look of the app" @@ -602,6 +672,16 @@ fun SettingsScreen( onSelect = viewModel::setAppThemeMode ) } + if (supportsDynamicColors) { + SwitchSettingsRow( + position = SegmentPosition.Last, + icon = Icons.Rounded.Wallpaper, + title = "Use dynamic colors", + subtitle = "Tint the app from your wallpaper", + checked = preferences.useDynamicColors, + onCheckedChange = viewModel::setUseDynamicColors + ) + } } } diff --git a/app/src/main/java/com/markreader/ui/screens/SettingsViewModel.kt b/app/src/main/java/com/markreader/ui/screens/SettingsViewModel.kt index b118f95..c506be3 100644 --- a/app/src/main/java/com/markreader/ui/screens/SettingsViewModel.kt +++ b/app/src/main/java/com/markreader/ui/screens/SettingsViewModel.kt @@ -27,6 +27,10 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application viewModelScope.launch { repository.setAppThemeMode(theme) } } + fun setUseDynamicColors(enabled: Boolean) { + viewModelScope.launch { repository.setUseDynamicColors(enabled) } + } + fun setReaderLightTheme(theme: ReaderThemePreference) { viewModelScope.launch { repository.setReaderLightTheme(theme) } } diff --git a/app/src/main/java/com/markreader/ui/screens/ViewerScreen.kt b/app/src/main/java/com/markreader/ui/screens/ViewerScreen.kt index 3dba549..5af3eb2 100644 --- a/app/src/main/java/com/markreader/ui/screens/ViewerScreen.kt +++ b/app/src/main/java/com/markreader/ui/screens/ViewerScreen.kt @@ -165,11 +165,19 @@ fun ViewerScreen( else -> "Rendered mode" } - val dynamicLightScheme = remember(context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicLightColorScheme(context) else null + val dynamicLightScheme = remember(context, prefs.useDynamicColors) { + if (prefs.useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + dynamicLightColorScheme(context) + } else { + null + } } - val dynamicDarkScheme = remember(context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicDarkColorScheme(context) else null + val dynamicDarkScheme = remember(context, prefs.useDynamicColors) { + if (prefs.useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + dynamicDarkColorScheme(context) + } else { + null + } } val (lightReaderColors, darkReaderColors) = resolveReaderColors(