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.ModelLoadingViewModel
import com.example.llama.revamp.viewmodel.ModelSelectionViewModel import com.example.llama.revamp.viewmodel.ModelSelectionViewModel
import com.example.llama.revamp.viewmodel.ModelsManagementViewModel 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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -58,13 +58,17 @@ import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
LlamaTheme { val settingsViewModel: SettingsViewModel = hiltViewModel()
val themeMode by settingsViewModel.themeMode.collectAsState()
LlamaTheme(themeMode) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
AppContent() AppContent(settingsViewModel)
} }
} }
} }
@ -73,8 +77,8 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun AppContent( fun AppContent(
settingsViewModel: SettingsViewModel,
mainViewModel: MainViewModel = hiltViewModel(), mainViewModel: MainViewModel = hiltViewModel(),
performanceViewModel: PerformanceViewModel = hiltViewModel(),
modelSelectionViewModel: ModelSelectionViewModel = hiltViewModel(), modelSelectionViewModel: ModelSelectionViewModel = hiltViewModel(),
modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(), modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(),
benchmarkViewModel: BenchmarkViewModel = hiltViewModel(), benchmarkViewModel: BenchmarkViewModel = hiltViewModel(),
@ -88,10 +92,10 @@ fun AppContent(
val engineState by mainViewModel.engineState.collectAsState() val engineState by mainViewModel.engineState.collectAsState()
// Metric states for scaffolds // Metric states for scaffolds
val memoryUsage by performanceViewModel.memoryUsage.collectAsState() val memoryUsage by settingsViewModel.memoryUsage.collectAsState()
val temperatureInfo by performanceViewModel.temperatureMetrics.collectAsState() val temperatureInfo by settingsViewModel.temperatureMetrics.collectAsState()
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState() val useFahrenheit by settingsViewModel.useFahrenheitUnit.collectAsState()
val storageMetrics by performanceViewModel.storageMetrics.collectAsState() val storageMetrics by settingsViewModel.storageMetrics.collectAsState()
// Navigation // Navigation
val navController = rememberNavController() val navController = rememberNavController()
@ -293,9 +297,6 @@ fun AppContent(
// Model Selection Screen // Model Selection Screen
composable(AppDestinations.MODEL_SELECTION_ROUTE) { composable(AppDestinations.MODEL_SELECTION_ROUTE) {
ModelSelectionScreen( ModelSelectionScreen(
onNavigateBack = {
navigationActions.navigateUp()
},
onModelConfirmed = { modelInfo -> onModelConfirmed = { modelInfo ->
navigationActions.navigateToModelLoading() navigationActions.navigateToModelLoading()
}, },
@ -371,7 +372,9 @@ fun AppContent(
// Settings General Screen // Settings General Screen
composable(AppDestinations.SETTINGS_GENERAL_ROUTE) { composable(AppDestinations.SETTINGS_GENERAL_ROUTE) {
SettingsGeneralScreen() SettingsGeneralScreen(
viewModel = settingsViewModel
)
} }
// Models Management Screen // 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.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -28,9 +29,15 @@ class UserPreferences @Inject constructor (
val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled") val PERFORMANCE_MONITORING_ENABLED = booleanPreferencesKey("performance_monitoring_enabled")
val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature") val USE_FAHRENHEIT_TEMPERATURE = booleanPreferencesKey("use_fahrenheit_temperature")
val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms") val MONITORING_INTERVAL_MS = longPreferencesKey("monitoring_interval_ms")
val THEME_MODE = intPreferencesKey("theme_mode")
// Default values // Default values
const val DEFAULT_MONITORING_INTERVAL_MS = 5000L 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> { fun isPerformanceMonitoringEnabled(): Flow<Boolean> {
return context.dataStore.data.map { preferences -> 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> { fun usesFahrenheitTemperature(): Flow<Boolean> {
return context.dataStore.data.map { preferences -> 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 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.Card
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme 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.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,20 +23,21 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.llama.revamp.APP_NAME 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 * Screen for general app settings
*/ */
@Composable @Composable
fun SettingsGeneralScreen( fun SettingsGeneralScreen(
performanceViewModel: PerformanceViewModel = hiltViewModel(), viewModel: SettingsViewModel,
) { ) {
// Collect state from ViewModel // Collect state from ViewModel
val isMonitoringEnabled by performanceViewModel.isMonitoringEnabled.collectAsState() val isMonitoringEnabled by viewModel.isMonitoringEnabled.collectAsState()
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState() val useFahrenheit by viewModel.useFahrenheitUnit.collectAsState()
val themeMode by viewModel.themeMode.collectAsState()
Column( Column(
modifier = Modifier modifier = Modifier
@ -44,28 +48,66 @@ fun SettingsGeneralScreen(
SettingsCategory(title = "Performance Monitoring") { SettingsCategory(title = "Performance Monitoring") {
SettingsSwitch( SettingsSwitch(
title = "Enable Monitoring", title = "Enable Monitoring",
description = "Display memory, battery and temperature information", description = "Display memory, battery and temperature info",
checked = isMonitoringEnabled, checked = isMonitoringEnabled,
onCheckedChange = { performanceViewModel.setMonitoringEnabled(it) } onCheckedChange = { viewModel.setMonitoringEnabled(it) }
) )
SettingsSwitch( SettingsSwitch(
title = "Use Fahrenheit", title = "Use Fahrenheit",
description = "Display temperature in Fahrenheit instead of Celsius", description = "Display temperature in Fahrenheit instead of Celsius",
checked = useFahrenheit, checked = useFahrenheit,
onCheckedChange = { performanceViewModel.setUseFahrenheitUnit(it) } onCheckedChange = { viewModel.setUseFahrenheitUnit(it) }
) )
} }
SettingsCategory(title = "Theme") { SettingsCategory(title = "Theme") {
SettingsSwitch( Column(
title = "Dark Theme", modifier = Modifier
description = "Use dark theme throughout the app", .fillMaxWidth()
checked = true, // This would be connected to theme state in a real app .padding(16.dp)
onCheckedChange = { ) {
/* TODO-hyin: Implement theme switching between Auto, Light and Dark */ 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") { 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_surfaceTint = Color(0xFFCFBCFF)
val md_theme_dark_outlineVariant = Color(0xFF49454E) val md_theme_dark_outlineVariant = Color(0xFF49454E)
val md_theme_dark_scrim = Color(0xFF000000) 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 package com.example.llama.revamp.ui.theme
import android.app.Activity import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme 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.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.example.llama.revamp.data.preferences.UserPreferences
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
// TODO-han.yin: support more / custom color palettes?
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary, primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary, onPrimary = md_theme_light_onPrimary,
@ -83,19 +83,18 @@ private val DarkColorScheme = darkColorScheme(
@Composable @Composable
fun LlamaTheme( fun LlamaTheme(
darkTheme: Boolean = isSystemInDarkTheme(), themeMode: Int = UserPreferences.THEME_MODE_AUTO,
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val darkTheme = when (themeMode) {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { UserPreferences.THEME_MODE_LIGHT -> false
UserPreferences.THEME_MODE_DARK -> true
else -> isSystemInDarkTheme()
}
val context = LocalContext.current val context = LocalContext.current
val colorScheme =
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {

View File

@ -22,7 +22,7 @@ import javax.inject.Inject
* ViewModel that manages performance monitoring for the app. * ViewModel that manages performance monitoring for the app.
*/ */
@HiltViewModel @HiltViewModel
class PerformanceViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val userPreferences: UserPreferences, private val userPreferences: UserPreferences,
private val performanceMonitor: PerformanceMonitor, private val performanceMonitor: PerformanceMonitor,
private val modelRepository: ModelRepository, private val modelRepository: ModelRepository,
@ -54,17 +54,27 @@ class PerformanceViewModel @Inject constructor(
private val _monitoringInterval = MutableStateFlow(5000L) private val _monitoringInterval = MutableStateFlow(5000L)
val monitoringInterval: StateFlow<Long> = _monitoringInterval.asStateFlow() val monitoringInterval: StateFlow<Long> = _monitoringInterval.asStateFlow()
private val _themeMode = MutableStateFlow(UserPreferences.THEME_MODE_AUTO)
val themeMode: StateFlow<Int> = _themeMode.asStateFlow()
init { init {
viewModelScope.launch { viewModelScope.launch {
// Load user preferences // Load user preferences
_isMonitoringEnabled.value = userPreferences.isPerformanceMonitoringEnabled().first() _isMonitoringEnabled.value = userPreferences.isPerformanceMonitoringEnabled().first()
_useFahrenheitUnit.value = userPreferences.usesFahrenheitTemperature().first() _useFahrenheitUnit.value = userPreferences.usesFahrenheitTemperature().first()
_monitoringInterval.value = userPreferences.getMonitoringInterval().first() _monitoringInterval.value = userPreferences.getMonitoringInterval().first()
_themeMode.value = userPreferences.getThemeMode().first()
// Start monitoring if enabled // Start monitoring if enabled
if (_isMonitoringEnabled.value) { if (_isMonitoringEnabled.value) {
startMonitoring() startMonitoring()
} }
viewModelScope.launch {
userPreferences.getThemeMode().collect { mode ->
_themeMode.value = mode
}
}
} }
} }
@ -144,4 +154,14 @@ class PerformanceViewModel @Inject constructor(
private fun isMonitoringActive(): Boolean { private fun isMonitoringActive(): Boolean {
return _isMonitoringEnabled.value return _isMonitoringEnabled.value
} }
/**
* Sets the theme mode.
*/
fun setThemeMode(mode: Int) {
viewModelScope.launch {
userPreferences.setThemeMode(mode)
_themeMode.value = mode
}
}
} }