Merge pull request #236 from Hy4ri/feat/issue-208-theme-presets
feat(#208): add Monochrome, Gruvbox, Catppuccin and AMOLED theme presets
This commit is contained in:
6 files changed
+345
-16
No files matched your search
@@ -24,7 +24,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
val themePreference by AuthManager.themePreferenceFlow.collectAsState()
|
val themePreference by AuthManager.themePreferenceFlow.collectAsState()
|
||||||
HermesControlTheme(themePreference = themePreference) {
|
val useDynamicColors by AuthManager.useDynamicColorsFlow.collectAsState()
|
||||||
|
val themePreset by AuthManager.themePresetFlow.collectAsState()
|
||||||
|
HermesControlTheme(
|
||||||
|
themePreference = themePreference,
|
||||||
|
useDynamicColors = useDynamicColors,
|
||||||
|
themePreset = themePreset,
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.SharedPreferences
|
|||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
|
import com.m57.hermescontrol.theme.ThemePreset
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -28,6 +29,8 @@ object AuthManager {
|
|||||||
private const val KEY_TYPING_EFFECT_DELAY_MS = "typing_effect_delay_ms"
|
private const val KEY_TYPING_EFFECT_DELAY_MS = "typing_effect_delay_ms"
|
||||||
private const val KEY_CONNECTION_PROFILES = "connection_profiles"
|
private const val KEY_CONNECTION_PROFILES = "connection_profiles"
|
||||||
private const val KEY_SELECTED_PROFILE_ID = "selected_profile_id"
|
private const val KEY_SELECTED_PROFILE_ID = "selected_profile_id"
|
||||||
|
private const val KEY_USE_DYNAMIC_COLORS = "use_dynamic_colors"
|
||||||
|
private const val KEY_THEME_PRESET = "theme_preset"
|
||||||
|
|
||||||
private const val DEFAULT_HOST = "127.0.0.1"
|
private const val DEFAULT_HOST = "127.0.0.1"
|
||||||
private const val DEFAULT_PORT = 9119
|
private const val DEFAULT_PORT = 9119
|
||||||
@@ -44,6 +47,11 @@ object AuthManager {
|
|||||||
private val _themePreferenceFlow = MutableStateFlow<ThemePreference>(ThemePreference.SYSTEM)
|
private val _themePreferenceFlow = MutableStateFlow<ThemePreference>(ThemePreference.SYSTEM)
|
||||||
val themePreferenceFlow: StateFlow<ThemePreference> = _themePreferenceFlow.asStateFlow()
|
val themePreferenceFlow: StateFlow<ThemePreference> = _themePreferenceFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val _useDynamicColorsFlow = MutableStateFlow<Boolean>(true)
|
||||||
|
val useDynamicColorsFlow: StateFlow<Boolean> = _useDynamicColorsFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val _themePresetFlow = MutableStateFlow<ThemePreset>(ThemePreset.DEFAULT)
|
||||||
|
val themePresetFlow: StateFlow<ThemePreset> = _themePresetFlow.asStateFlow()
|
||||||
private val gson = com.google.gson.Gson()
|
private val gson = com.google.gson.Gson()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +79,8 @@ object AuthManager {
|
|||||||
|
|
||||||
_bottomNavItemsFlow.value = getBottomNavItems()
|
_bottomNavItemsFlow.value = getBottomNavItems()
|
||||||
_themePreferenceFlow.value = getThemePreference()
|
_themePreferenceFlow.value = getThemePreference()
|
||||||
|
_useDynamicColorsFlow.value = isUseDynamicColors()
|
||||||
|
_themePresetFlow.value = getThemePreset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +223,23 @@ object AuthManager {
|
|||||||
_themePreferenceFlow.value = theme
|
_themePreferenceFlow.value = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isUseDynamicColors(): Boolean = requirePrefs().getBoolean(KEY_USE_DYNAMIC_COLORS, true)
|
||||||
|
|
||||||
|
fun setUseDynamicColors(value: Boolean) {
|
||||||
|
requirePrefs().edit().putBoolean(KEY_USE_DYNAMIC_COLORS, value).apply()
|
||||||
|
_useDynamicColorsFlow.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThemePreset(): ThemePreset =
|
||||||
|
requirePrefs().getString(KEY_THEME_PRESET, ThemePreset.DEFAULT.name)?.let { name ->
|
||||||
|
runCatching { ThemePreset.valueOf(name) }.getOrNull()
|
||||||
|
} ?: ThemePreset.DEFAULT
|
||||||
|
|
||||||
|
fun setThemePreset(preset: ThemePreset) {
|
||||||
|
requirePrefs().edit().putString(KEY_THEME_PRESET, preset.name).apply()
|
||||||
|
_themePresetFlow.value = preset
|
||||||
|
}
|
||||||
|
|
||||||
/** Convenience: build the base URL from current host + port.
|
/** Convenience: build the base URL from current host + port.
|
||||||
* NOTE: Uses plain http:// — intended for trusted local network only.
|
* NOTE: Uses plain http:// — intended for trusted local network only.
|
||||||
* Exposing the host to a hostile LAN risks token interception. */
|
* Exposing the host to a hostile LAN risks token interception. */
|
||||||
|
|||||||
@@ -15,19 +15,13 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
|
|
||||||
enum class ThemePreference { SYSTEM, LIGHT, DARK }
|
enum class ThemePreference { SYSTEM, LIGHT, DARK }
|
||||||
|
|
||||||
|
enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED }
|
||||||
|
|
||||||
val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
||||||
|
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true to opt into Material You dynamic color (Android 12+).
|
* Set to true to opt into Material You dynamic color (Android 12+).
|
||||||
*
|
|
||||||
* On API 31+ the app derives its entire colour scheme from the user's
|
|
||||||
* wallpaper — primary, secondary, tertiary, surfaces, the lot. On older
|
|
||||||
* devices the [HermesDarkColorScheme] / [HermesLightColorScheme] brand
|
|
||||||
* palette is used instead.
|
|
||||||
*
|
|
||||||
* Semantic status colours (success / warning / error / info) and chat-bubble
|
|
||||||
* colours are ALWAYS brand-defined regardless of dynamic colour — see
|
|
||||||
* [Color.kt] and the `LocalHermesStatusColors` provider below.
|
|
||||||
*/
|
*/
|
||||||
private const val ENABLE_DYNAMIC_COLOR = true
|
private const val ENABLE_DYNAMIC_COLOR = true
|
||||||
|
|
||||||
@@ -97,10 +91,144 @@ private val HermesLightColorScheme =
|
|||||||
scrim = LightScrim,
|
scrim = LightScrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val MonochromeDarkColorScheme =
|
||||||
|
darkColorScheme(
|
||||||
|
primary = Color.White,
|
||||||
|
onPrimary = Color.Black,
|
||||||
|
primaryContainer = Color(0xFF2B2B2B),
|
||||||
|
onPrimaryContainer = Color.White,
|
||||||
|
secondary = Color(0xFFB0B0B0),
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
secondaryContainer = Color(0xFF222222),
|
||||||
|
onSecondaryContainer = Color(0xFFE0E0E0),
|
||||||
|
background = Color(0xFF121212),
|
||||||
|
onBackground = Color(0xFFE0E0E0),
|
||||||
|
surface = Color(0xFF1E1E1E),
|
||||||
|
onSurface = Color(0xFFE0E0E0),
|
||||||
|
surfaceVariant = Color(0xFF2B2B2B),
|
||||||
|
onSurfaceVariant = Color(0xFFB0B0B0),
|
||||||
|
outline = Color(0xFF666666),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val MonochromeLightColorScheme =
|
||||||
|
lightColorScheme(
|
||||||
|
primary = Color.Black,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
primaryContainer = Color(0xFFE0E0E0),
|
||||||
|
onPrimaryContainer = Color.Black,
|
||||||
|
secondary = Color(0xFF4A4A4A),
|
||||||
|
onSecondary = Color.White,
|
||||||
|
secondaryContainer = Color(0xFFF0F0F0),
|
||||||
|
onSecondaryContainer = Color(0xFF111111),
|
||||||
|
background = Color(0xFFFFFFFF),
|
||||||
|
onBackground = Color(0xFF111111),
|
||||||
|
surface = Color(0xFFF5F5F5),
|
||||||
|
onSurface = Color(0xFF111111),
|
||||||
|
surfaceVariant = Color(0xFFE0E0E0),
|
||||||
|
onSurfaceVariant = Color(0xFF4A4A4A),
|
||||||
|
outline = Color(0xFF888888),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val GruvboxDarkColorScheme =
|
||||||
|
darkColorScheme(
|
||||||
|
primary = Color(0xFFFE8019),
|
||||||
|
onPrimary = Color(0xFF282828),
|
||||||
|
primaryContainer = Color(0xFFD65D0E),
|
||||||
|
onPrimaryContainer = Color(0xFFFBF1C7),
|
||||||
|
secondary = Color(0xFFFABD2F),
|
||||||
|
onSecondary = Color(0xFF282828),
|
||||||
|
secondaryContainer = Color(0xFF3C3836),
|
||||||
|
onSecondaryContainer = Color(0xFFBDAE93),
|
||||||
|
background = Color(0xFF282828),
|
||||||
|
onBackground = Color(0xFFFBF1C7),
|
||||||
|
surface = Color(0xFF32302F),
|
||||||
|
onSurface = Color(0xFFFBF1C7),
|
||||||
|
surfaceVariant = Color(0xFF3C3836),
|
||||||
|
onSurfaceVariant = Color(0xFFBDAE93),
|
||||||
|
outline = Color(0xFF665C54),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val GruvboxLightColorScheme =
|
||||||
|
lightColorScheme(
|
||||||
|
primary = Color(0xFFD65D0E),
|
||||||
|
onPrimary = Color(0xFFFBF1C7),
|
||||||
|
primaryContainer = Color(0xFFFE8019),
|
||||||
|
onPrimaryContainer = Color(0xFF282828),
|
||||||
|
secondary = Color(0xFF458588),
|
||||||
|
onSecondary = Color(0xFFFBF1C7),
|
||||||
|
secondaryContainer = Color(0xFFEBDBB2),
|
||||||
|
onSecondaryContainer = Color(0xFF3C3836),
|
||||||
|
background = Color(0xFFFBF1C7),
|
||||||
|
onBackground = Color(0xFF282828),
|
||||||
|
surface = Color(0xFFF2E5BC),
|
||||||
|
onSurface = Color(0xFF282828),
|
||||||
|
surfaceVariant = Color(0xFFEBDBB2),
|
||||||
|
onSurfaceVariant = Color(0xFF7C6F64),
|
||||||
|
outline = Color(0xFFA89984),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val CatppuccinDarkColorScheme =
|
||||||
|
darkColorScheme(
|
||||||
|
primary = Color(0xFFCBA6F7),
|
||||||
|
onPrimary = Color(0xFF11111B),
|
||||||
|
primaryContainer = Color(0xFF585B70),
|
||||||
|
onPrimaryContainer = Color(0xFFF5E0DC),
|
||||||
|
secondary = Color(0xFF89B4FA),
|
||||||
|
onSecondary = Color(0xFF11111B),
|
||||||
|
secondaryContainer = Color(0xFF313244),
|
||||||
|
onSecondaryContainer = Color(0xFFBAC2DE),
|
||||||
|
background = Color(0xFF1E1E2E),
|
||||||
|
onBackground = Color(0xFFCDD6F4),
|
||||||
|
surface = Color(0xFF252538),
|
||||||
|
onSurface = Color(0xFFCDD6F4),
|
||||||
|
surfaceVariant = Color(0xFF313244),
|
||||||
|
onSurfaceVariant = Color(0xFFA6ADC8),
|
||||||
|
outline = Color(0xFF585B70),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val CatppuccinLightColorScheme =
|
||||||
|
lightColorScheme(
|
||||||
|
primary = Color(0xFF8839EF),
|
||||||
|
onPrimary = Color(0xFFEFF1F5),
|
||||||
|
primaryContainer = Color(0xFFCCD0DA),
|
||||||
|
onPrimaryContainer = Color(0xFF4C4F69),
|
||||||
|
secondary = Color(0xFF1E66F5),
|
||||||
|
onSecondary = Color(0xFFEFF1F5),
|
||||||
|
secondaryContainer = Color(0xFFE6E9EF),
|
||||||
|
onSecondaryContainer = Color(0xFF4C4F69),
|
||||||
|
background = Color(0xFFEFF1F5),
|
||||||
|
onBackground = Color(0xFF4C4F69),
|
||||||
|
surface = Color(0xFFE6E9EF),
|
||||||
|
onSurface = Color(0xFF4C4F69),
|
||||||
|
surfaceVariant = Color(0xFFCCD0DA),
|
||||||
|
onSurfaceVariant = Color(0xFF6C6F85),
|
||||||
|
outline = Color(0xFF9CA0B0),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val AmoledDarkColorScheme =
|
||||||
|
darkColorScheme(
|
||||||
|
primary = HermesPurple,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
primaryContainer = Color(0xFF1A1040),
|
||||||
|
onPrimaryContainer = HermesPurpleOnContainer,
|
||||||
|
secondary = HermesAmber,
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
secondaryContainer = Color(0xFF2A2000),
|
||||||
|
onSecondaryContainer = HermesAmberLight,
|
||||||
|
background = Color.Black,
|
||||||
|
onBackground = Color(0xFFE8E6EE),
|
||||||
|
surface = Color(0xFF08080A),
|
||||||
|
onSurface = Color(0xFFE8E6EE),
|
||||||
|
surfaceVariant = Color(0xFF121218),
|
||||||
|
onSurfaceVariant = Color(0xFFB6B2C4),
|
||||||
|
outline = Color(0xFF3A3A4A),
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HermesControlTheme(
|
fun HermesControlTheme(
|
||||||
themePreference: ThemePreference = LocalThemePreference.current,
|
themePreference: ThemePreference = LocalThemePreference.current,
|
||||||
enableDynamicColor: Boolean = ENABLE_DYNAMIC_COLOR,
|
useDynamicColors: Boolean = true,
|
||||||
|
themePreset: ThemePreset = ThemePreset.DEFAULT,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val darkTheme =
|
val darkTheme =
|
||||||
@@ -111,17 +239,73 @@ fun HermesControlTheme(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val dynamicAvailable = enableDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
val dynamicAvailable = useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
val colorScheme =
|
val colorScheme =
|
||||||
when {
|
when {
|
||||||
dynamicAvailable && darkTheme -> dynamicDarkColorScheme(context)
|
dynamicAvailable && darkTheme -> dynamicDarkColorScheme(context)
|
||||||
dynamicAvailable && !darkTheme -> dynamicLightColorScheme(context)
|
dynamicAvailable && !darkTheme -> dynamicLightColorScheme(context)
|
||||||
darkTheme -> HermesDarkColorScheme
|
else -> {
|
||||||
else -> HermesLightColorScheme
|
when (themePreset) {
|
||||||
|
ThemePreset.DEFAULT -> if (darkTheme) HermesDarkColorScheme else HermesLightColorScheme
|
||||||
|
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkColorScheme else MonochromeLightColorScheme
|
||||||
|
ThemePreset.GRUVBOX -> if (darkTheme) GruvboxDarkColorScheme else GruvboxLightColorScheme
|
||||||
|
ThemePreset.CATPPUCCIN -> if (darkTheme) CatppuccinDarkColorScheme else CatppuccinLightColorScheme
|
||||||
|
ThemePreset.AMOLED -> if (darkTheme) AmoledDarkColorScheme else HermesLightColorScheme
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val statusColors =
|
val statusColors =
|
||||||
if (darkTheme) {
|
when {
|
||||||
|
themePreset == ThemePreset.GRUVBOX && darkTheme -> {
|
||||||
|
HermesStatusColors(
|
||||||
|
success = Color(0xFFB8BB26),
|
||||||
|
successContainer = Color(0xFF32302F),
|
||||||
|
warning = Color(0xFFFABD2F),
|
||||||
|
warningContainer = Color(0xFF3C3836),
|
||||||
|
error = Color(0xFFFB4934),
|
||||||
|
errorContainer = Color(0xFF282828),
|
||||||
|
info = Color(0xFF83A598),
|
||||||
|
infoContainer = Color(0xFF3C3836),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
themePreset == ThemePreset.GRUVBOX && !darkTheme -> {
|
||||||
|
HermesStatusColors(
|
||||||
|
success = Color(0xFF98971A),
|
||||||
|
successContainer = Color(0xFFEBDBB2),
|
||||||
|
warning = Color(0xFFD79921),
|
||||||
|
warningContainer = Color(0xFFEBDBB2),
|
||||||
|
error = Color(0xFFCC241D),
|
||||||
|
errorContainer = Color(0xFFEBDBB2),
|
||||||
|
info = Color(0xFF458588),
|
||||||
|
infoContainer = Color(0xFFEBDBB2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
themePreset == ThemePreset.CATPPUCCIN && darkTheme -> {
|
||||||
|
HermesStatusColors(
|
||||||
|
success = Color(0xFFA6E3A1),
|
||||||
|
successContainer = Color(0xFF313244),
|
||||||
|
warning = Color(0xFFF9E2AF),
|
||||||
|
warningContainer = Color(0xFF313244),
|
||||||
|
error = Color(0xFFF38BA8),
|
||||||
|
errorContainer = Color(0xFF313244),
|
||||||
|
info = Color(0xFF89B4FA),
|
||||||
|
infoContainer = Color(0xFF313244),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
themePreset == ThemePreset.CATPPUCCIN && !darkTheme -> {
|
||||||
|
HermesStatusColors(
|
||||||
|
success = Color(0xFF40A02B),
|
||||||
|
successContainer = Color(0xFFE6E9EF),
|
||||||
|
warning = Color(0xFFDF8E1D),
|
||||||
|
warningContainer = Color(0xFFE6E9EF),
|
||||||
|
error = Color(0xFFD20F39),
|
||||||
|
errorContainer = Color(0xFFE6E9EF),
|
||||||
|
info = Color(0xFF1E66F5),
|
||||||
|
infoContainer = Color(0xFFE6E9EF),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
darkTheme -> {
|
||||||
HermesStatusColors(
|
HermesStatusColors(
|
||||||
success = StatusGreen,
|
success = StatusGreen,
|
||||||
successContainer = StatusGreenContainer,
|
successContainer = StatusGreenContainer,
|
||||||
@@ -132,7 +316,8 @@ fun HermesControlTheme(
|
|||||||
info = StatusBlue,
|
info = StatusBlue,
|
||||||
infoContainer = StatusBlueContainer,
|
infoContainer = StatusBlueContainer,
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
HermesStatusColors(
|
HermesStatusColors(
|
||||||
success = Color(0xFF1B873A),
|
success = Color(0xFF1B873A),
|
||||||
successContainer = Color(0xFFD7F5E0),
|
successContainer = Color(0xFFD7F5E0),
|
||||||
@@ -144,8 +329,11 @@ fun HermesControlTheme(
|
|||||||
infoContainer = Color(0xFFD4E7FF),
|
infoContainer = Color(0xFFD4E7FF),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
LocalThemePreference provides themePreference,
|
||||||
|
LocalThemePreset provides themePreset,
|
||||||
LocalHermesStatusColors provides statusColors,
|
LocalHermesStatusColors provides statusColors,
|
||||||
LocalSpacing provides SpacingDefaults,
|
LocalSpacing provides SpacingDefaults,
|
||||||
LocalMotion provides MotionDefaults,
|
LocalMotion provides MotionDefaults,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.m57.hermescontrol.BuildConfig
|
import com.m57.hermescontrol.BuildConfig
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
|
import com.m57.hermescontrol.theme.ThemePreset
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -322,6 +323,88 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_item_use_dynamic_colors),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_desc_use_dynamic_colors),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = state.useDynamicColors,
|
||||||
|
onCheckedChange = viewModel::onUseDynamicColorsChange,
|
||||||
|
colors =
|
||||||
|
SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_item_theme_preset),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
var presetsExpanded by remember { mutableStateOf(false) }
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { presetsExpanded = true },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
when (state.themePreset) {
|
||||||
|
ThemePreset.DEFAULT -> stringResource(R.string.theme_preset_default)
|
||||||
|
ThemePreset.MONOCHROME -> stringResource(R.string.theme_preset_monochrome)
|
||||||
|
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox)
|
||||||
|
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin)
|
||||||
|
ThemePreset.AMOLED -> stringResource(R.string.theme_preset_amoled)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = presetsExpanded,
|
||||||
|
onDismissRequest = { presetsExpanded = false },
|
||||||
|
modifier = Modifier.fillMaxWidth(0.85f),
|
||||||
|
) {
|
||||||
|
ThemePreset.entries.forEach { preset ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
when (preset) {
|
||||||
|
ThemePreset.DEFAULT -> stringResource(R.string.theme_preset_default)
|
||||||
|
ThemePreset.MONOCHROME -> stringResource(R.string.theme_preset_monochrome)
|
||||||
|
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox)
|
||||||
|
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin)
|
||||||
|
ThemePreset.AMOLED -> stringResource(R.string.theme_preset_amoled)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.onThemePresetChange(preset)
|
||||||
|
presetsExpanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat section
|
// Chat section
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.remote.ApiClient
|
|||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
|
import com.m57.hermescontrol.theme.ThemePreset
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -22,6 +23,8 @@ data class SettingsUiState(
|
|||||||
val token: String = "",
|
val token: String = "",
|
||||||
val autoReconnect: Boolean = true,
|
val autoReconnect: Boolean = true,
|
||||||
val themePreference: ThemePreference = ThemePreference.SYSTEM,
|
val themePreference: ThemePreference = ThemePreference.SYSTEM,
|
||||||
|
val useDynamicColors: Boolean = true,
|
||||||
|
val themePreset: ThemePreset = ThemePreset.DEFAULT,
|
||||||
val isTesting: Boolean = false,
|
val isTesting: Boolean = false,
|
||||||
val testResult: String? = null,
|
val testResult: String? = null,
|
||||||
val isSaved: Boolean = false,
|
val isSaved: Boolean = false,
|
||||||
@@ -53,6 +56,8 @@ class SettingsViewModel : ViewModel() {
|
|||||||
// choice from persistent storage on init. Previously this slot
|
// choice from persistent storage on init. Previously this slot
|
||||||
// was missing — always defaulted to ThemePreference.SYSTEM.
|
// was missing — always defaulted to ThemePreference.SYSTEM.
|
||||||
themePreference = AuthManager.getThemePreference(),
|
themePreference = AuthManager.getThemePreference(),
|
||||||
|
useDynamicColors = AuthManager.isUseDynamicColors(),
|
||||||
|
themePreset = AuthManager.getThemePreset(),
|
||||||
selectedNavItems = AuthManager.getBottomNavItems(),
|
selectedNavItems = AuthManager.getBottomNavItems(),
|
||||||
typingEffectEnabled = AuthManager.isTypingEffectEnabled(),
|
typingEffectEnabled = AuthManager.isTypingEffectEnabled(),
|
||||||
typingEffectDelayMs = AuthManager.getTypingEffectDelayMs(),
|
typingEffectDelayMs = AuthManager.getTypingEffectDelayMs(),
|
||||||
@@ -122,6 +127,16 @@ class SettingsViewModel : ViewModel() {
|
|||||||
AuthManager.setThemePreference(theme)
|
AuthManager.setThemePreference(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onUseDynamicColorsChange(enabled: Boolean) {
|
||||||
|
_uiState.update { it.copy(useDynamicColors = enabled, isSaved = false) }
|
||||||
|
AuthManager.setUseDynamicColors(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onThemePresetChange(preset: ThemePreset) {
|
||||||
|
_uiState.update { it.copy(themePreset = preset, isSaved = false) }
|
||||||
|
AuthManager.setThemePreset(preset)
|
||||||
|
}
|
||||||
|
|
||||||
fun onTypingEffectEnabledChange(enabled: Boolean) {
|
fun onTypingEffectEnabledChange(enabled: Boolean) {
|
||||||
_uiState.update { it.copy(typingEffectEnabled = enabled, isSaved = false) }
|
_uiState.update { it.copy(typingEffectEnabled = enabled, isSaved = false) }
|
||||||
}
|
}
|
||||||
@@ -188,6 +203,8 @@ class SettingsViewModel : ViewModel() {
|
|||||||
// B6 (Jun 18 2026, kanban t_86e9be9b): persist theme choice so it
|
// B6 (Jun 18 2026, kanban t_86e9be9b): persist theme choice so it
|
||||||
// survives a cold start (was previously dropped on save()).
|
// survives a cold start (was previously dropped on save()).
|
||||||
AuthManager.setThemePreference(state.themePreference)
|
AuthManager.setThemePreference(state.themePreference)
|
||||||
|
AuthManager.setUseDynamicColors(state.useDynamicColors)
|
||||||
|
AuthManager.setThemePreset(state.themePreset)
|
||||||
AuthManager.setTypingEffectEnabled(state.typingEffectEnabled)
|
AuthManager.setTypingEffectEnabled(state.typingEffectEnabled)
|
||||||
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
|
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
|
||||||
ApiClient.rebuild()
|
ApiClient.rebuild()
|
||||||
|
|||||||
@@ -148,6 +148,14 @@
|
|||||||
<string name="theme_system">System</string>
|
<string name="theme_system">System</string>
|
||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
|
<string name="settings_item_use_dynamic_colors">Dynamic Colors</string>
|
||||||
|
<string name="settings_desc_use_dynamic_colors">Derive scheme from wallpaper (Android 12+)</string>
|
||||||
|
<string name="settings_item_theme_preset">Preset Theme</string>
|
||||||
|
<string name="theme_preset_default">Default (Voltage Purple)</string>
|
||||||
|
<string name="theme_preset_monochrome">Monochrome</string>
|
||||||
|
<string name="theme_preset_gruvbox">Gruvbox</string>
|
||||||
|
<string name="theme_preset_catppuccin">Catppuccin</string>
|
||||||
|
<string name="theme_preset_amoled">AMOLED Black (Dark only)</string>
|
||||||
|
|
||||||
<!-- Connect Screen -->
|
<!-- Connect Screen -->
|
||||||
<string name="connect_selected_profile">Profile: %1$s</string>
|
<string name="connect_selected_profile">Profile: %1$s</string>
|
||||||
|
|||||||
Reference in new issue
Block a user