UI: finally support theme modes; remove hardcoded color schemes, default to dynamic color scheme implementation

This commit is contained in:
Han Yin 2025-04-20 21:21:30 -07:00
parent a8dc825aef
commit 2b3ba770dd
6 changed files with 131 additions and 47 deletions

View File

@ -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

View File

@ -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<Boolean> {
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<Boolean> {
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<Int> {
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
}
}
}

View File

@ -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") {

View File

@ -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)

View File

@ -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,19 +83,18 @@ 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 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)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {

View File

@ -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<Long> = _monitoringInterval.asStateFlow()
private val _themeMode = MutableStateFlow(UserPreferences.THEME_MODE_AUTO)
val themeMode: StateFlow<Int> = _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
}
}
}