From 2b3ba770ddf6bd86ba6c74f776de13615cadf6f9 Mon Sep 17 00:00:00 2001 From: Han Yin Date: Sun, 20 Apr 2025 21:21:30 -0700 Subject: [PATCH] UI: finally support theme modes; remove hardcoded color schemes, default to dynamic color scheme implementation --- .../com/example/llama/revamp/MainActivity.kt | 27 +++---- .../data/preferences/UserPreferences.kt | 29 +++++++- .../ui/screens/SettingsGeneralScreen.kt | 72 +++++++++++++++---- .../example/llama/revamp/ui/theme/Color.kt | 5 -- .../example/llama/revamp/ui/theme/Theme.kt | 23 +++--- ...manceViewModel.kt => SettingsViewModel.kt} | 22 +++++- 6 files changed, 131 insertions(+), 47 deletions(-) rename examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/{PerformanceViewModel.kt => SettingsViewModel.kt} (88%) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt index c9b429071f..1f17b7b7d2 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt @@ -50,7 +50,7 @@ import com.example.llama.revamp.viewmodel.MainViewModel import com.example.llama.revamp.viewmodel.ModelLoadingViewModel import com.example.llama.revamp.viewmodel.ModelSelectionViewModel import com.example.llama.revamp.viewmodel.ModelsManagementViewModel -import com.example.llama.revamp.viewmodel.PerformanceViewModel +import com.example.llama.revamp.viewmodel.SettingsViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -58,13 +58,17 @@ import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { - LlamaTheme { + val settingsViewModel: SettingsViewModel = hiltViewModel() + val themeMode by settingsViewModel.themeMode.collectAsState() + + LlamaTheme(themeMode) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - AppContent() + AppContent(settingsViewModel) } } } @@ -73,8 +77,8 @@ class MainActivity : ComponentActivity() { @Composable fun AppContent( + settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel = hiltViewModel(), - performanceViewModel: PerformanceViewModel = hiltViewModel(), modelSelectionViewModel: ModelSelectionViewModel = hiltViewModel(), modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(), benchmarkViewModel: BenchmarkViewModel = hiltViewModel(), @@ -88,10 +92,10 @@ fun AppContent( val engineState by mainViewModel.engineState.collectAsState() // Metric states for scaffolds - val memoryUsage by performanceViewModel.memoryUsage.collectAsState() - val temperatureInfo by performanceViewModel.temperatureMetrics.collectAsState() - val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState() - val storageMetrics by performanceViewModel.storageMetrics.collectAsState() + val memoryUsage by settingsViewModel.memoryUsage.collectAsState() + val temperatureInfo by settingsViewModel.temperatureMetrics.collectAsState() + val useFahrenheit by settingsViewModel.useFahrenheitUnit.collectAsState() + val storageMetrics by settingsViewModel.storageMetrics.collectAsState() // Navigation val navController = rememberNavController() @@ -293,9 +297,6 @@ fun AppContent( // Model Selection Screen composable(AppDestinations.MODEL_SELECTION_ROUTE) { ModelSelectionScreen( - onNavigateBack = { - navigationActions.navigateUp() - }, onModelConfirmed = { modelInfo -> navigationActions.navigateToModelLoading() }, @@ -371,7 +372,9 @@ fun AppContent( // Settings General Screen composable(AppDestinations.SETTINGS_GENERAL_ROUTE) { - SettingsGeneralScreen() + SettingsGeneralScreen( + viewModel = settingsViewModel + ) } // Models Management Screen diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/preferences/UserPreferences.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/preferences/UserPreferences.kt index 1759326b5b..a69b00838a 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/preferences/UserPreferences.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/preferences/UserPreferences.kt @@ -5,6 +5,7 @@ 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.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStore import dagger.hilt.android.qualifiers.ApplicationContext @@ -28,9 +29,15 @@ class UserPreferences @Inject constructor ( val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled") val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature") val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms") + val THEME_MODE = intPreferencesKey("theme_mode") // Default values const val DEFAULT_MONITORING_INTERVAL_MS = 5000L + + // Theme mode values + const val THEME_MODE_AUTO = 0 + const val THEME_MODE_LIGHT = 1 + const val THEME_MODE_DARK = 2 } /** @@ -38,7 +45,7 @@ class UserPreferences @Inject constructor ( */ fun isPerformanceMonitoringEnabled(): Flow { return context.dataStore.data.map { preferences -> - preferences[PERFORMANCE_MONITORING_ENABLED] ?: true + preferences[PERFORMANCE_MONITORING_ENABLED] != false } } @@ -56,7 +63,7 @@ class UserPreferences @Inject constructor ( */ fun usesFahrenheitTemperature(): Flow { return context.dataStore.data.map { preferences -> - preferences[USE_FAHRENHEIT_TEMPERATURE] ?: false + preferences[USE_FAHRENHEIT_TEMPERATURE] == true } } @@ -88,4 +95,22 @@ class UserPreferences @Inject constructor ( preferences[MONITORING_INTERVAL_MS] = intervalMs } } + + /** + * Gets the current theme mode. + */ + fun getThemeMode(): Flow { + return context.dataStore.data.map { preferences -> + preferences[THEME_MODE] ?: THEME_MODE_AUTO + } + } + + /** + * Sets the theme mode. + */ + suspend fun setThemeMode(mode: Int) { + context.dataStore.edit { preferences -> + preferences[THEME_MODE] = mode + } + } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsGeneralScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsGeneralScreen.kt index 99ad7d49b2..d34f3e4205 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsGeneralScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsGeneralScreen.kt @@ -12,6 +12,9 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,20 +23,21 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.example.llama.revamp.APP_NAME -import com.example.llama.revamp.viewmodel.PerformanceViewModel +import com.example.llama.revamp.data.preferences.UserPreferences +import com.example.llama.revamp.viewmodel.SettingsViewModel /** * Screen for general app settings */ @Composable fun SettingsGeneralScreen( - performanceViewModel: PerformanceViewModel = hiltViewModel(), + viewModel: SettingsViewModel, ) { // Collect state from ViewModel - val isMonitoringEnabled by performanceViewModel.isMonitoringEnabled.collectAsState() - val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState() + val isMonitoringEnabled by viewModel.isMonitoringEnabled.collectAsState() + val useFahrenheit by viewModel.useFahrenheitUnit.collectAsState() + val themeMode by viewModel.themeMode.collectAsState() Column( modifier = Modifier @@ -44,28 +48,66 @@ fun SettingsGeneralScreen( SettingsCategory(title = "Performance Monitoring") { SettingsSwitch( title = "Enable Monitoring", - description = "Display memory, battery and temperature information", + description = "Display memory, battery and temperature info", checked = isMonitoringEnabled, - onCheckedChange = { performanceViewModel.setMonitoringEnabled(it) } + onCheckedChange = { viewModel.setMonitoringEnabled(it) } ) SettingsSwitch( title = "Use Fahrenheit", description = "Display temperature in Fahrenheit instead of Celsius", checked = useFahrenheit, - onCheckedChange = { performanceViewModel.setUseFahrenheitUnit(it) } + onCheckedChange = { viewModel.setUseFahrenheitUnit(it) } ) } SettingsCategory(title = "Theme") { - SettingsSwitch( - title = "Dark Theme", - description = "Use dark theme throughout the app", - checked = true, // This would be connected to theme state in a real app - onCheckedChange = { - /* TODO-hyin: Implement theme switching between Auto, Light and Dark */ + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = "Theme Mode", + style = MaterialTheme.typography.titleMedium + ) + + Text( + text = "Follow system setting or override with your choice", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() + ) { + SegmentedButton( + selected = themeMode == UserPreferences.THEME_MODE_AUTO, + onClick = { viewModel.setThemeMode(UserPreferences.THEME_MODE_AUTO) }, + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 3) + ) { + Text("Auto") + } + + SegmentedButton( + selected = themeMode == UserPreferences.THEME_MODE_LIGHT, + onClick = { viewModel.setThemeMode(UserPreferences.THEME_MODE_LIGHT) }, + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 3) + ) { + Text("Light") + } + + SegmentedButton( + selected = themeMode == UserPreferences.THEME_MODE_DARK, + onClick = { viewModel.setThemeMode(UserPreferences.THEME_MODE_DARK) }, + shape = SegmentedButtonDefaults.itemShape(index = 2, count = 3) + ) { + Text("Dark") + } } - ) + } } SettingsCategory(title = "About") { diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Color.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Color.kt index 615280cc7d..4b53aedd97 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Color.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Color.kt @@ -64,8 +64,3 @@ val md_theme_dark_inversePrimary = Color(0xFF6750A4) val md_theme_dark_surfaceTint = Color(0xFFCFBCFF) val md_theme_dark_outlineVariant = Color(0xFF49454E) val md_theme_dark_scrim = Color(0xFF000000) - -// Additional app-specific colors -val CriticalColor = Color(0xFFFF0000) -val WarningColor = Color(0xFFFFA000) -val SuccessColor = Color(0xFF388E3C) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Theme.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Theme.kt index ae6113e337..d913c03125 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Theme.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/theme/Theme.kt @@ -1,8 +1,6 @@ package com.example.llama.revamp.ui.theme - import android.app.Activity -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme @@ -15,8 +13,10 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import com.example.llama.revamp.data.preferences.UserPreferences import com.google.accompanist.systemuicontroller.rememberSystemUiController +// TODO-han.yin: support more / custom color palettes? private val LightColorScheme = lightColorScheme( primary = md_theme_light_primary, onPrimary = md_theme_light_onPrimary, @@ -83,20 +83,19 @@ private val DarkColorScheme = darkColorScheme( @Composable fun LlamaTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = false, + themeMode: Int = UserPreferences.THEME_MODE_AUTO, content: @Composable () -> Unit ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme + val darkTheme = when (themeMode) { + UserPreferences.THEME_MODE_LIGHT -> false + UserPreferences.THEME_MODE_DARK -> true + else -> isSystemInDarkTheme() } + val context = LocalContext.current + val colorScheme = + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + val view = LocalView.current if (!view.isInEditMode) { val systemUiController = rememberSystemUiController() diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/PerformanceViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/SettingsViewModel.kt similarity index 88% rename from examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/PerformanceViewModel.kt rename to examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/SettingsViewModel.kt index 247358a358..3596fbc271 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/PerformanceViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/SettingsViewModel.kt @@ -22,7 +22,7 @@ import javax.inject.Inject * ViewModel that manages performance monitoring for the app. */ @HiltViewModel -class PerformanceViewModel @Inject constructor( +class SettingsViewModel @Inject constructor( private val userPreferences: UserPreferences, private val performanceMonitor: PerformanceMonitor, private val modelRepository: ModelRepository, @@ -54,17 +54,27 @@ class PerformanceViewModel @Inject constructor( private val _monitoringInterval = MutableStateFlow(5000L) val monitoringInterval: StateFlow = _monitoringInterval.asStateFlow() + private val _themeMode = MutableStateFlow(UserPreferences.THEME_MODE_AUTO) + val themeMode: StateFlow = _themeMode.asStateFlow() + init { viewModelScope.launch { // Load user preferences _isMonitoringEnabled.value = userPreferences.isPerformanceMonitoringEnabled().first() _useFahrenheitUnit.value = userPreferences.usesFahrenheitTemperature().first() _monitoringInterval.value = userPreferences.getMonitoringInterval().first() + _themeMode.value = userPreferences.getThemeMode().first() // Start monitoring if enabled if (_isMonitoringEnabled.value) { startMonitoring() } + + viewModelScope.launch { + userPreferences.getThemeMode().collect { mode -> + _themeMode.value = mode + } + } } } @@ -144,4 +154,14 @@ class PerformanceViewModel @Inject constructor( private fun isMonitoringActive(): Boolean { return _isMonitoringEnabled.value } + + /** + * Sets the theme mode. + */ + fun setThemeMode(mode: Int) { + viewModelScope.launch { + userPreferences.setThemeMode(mode) + _themeMode.value = mode + } + } }