From c6bf1ecbf203780066f29cee7535fc3282c01ba9 Mon Sep 17 00:00:00 2001 From: Nino Date: Tue, 4 Aug 2026 14:13:35 -0600 Subject: [PATCH] v1.0.4: ajuste de temas de colores (Catppuccin, Gruvbox, Nord, Monokai, etc.) --- app/build.gradle | 4 +- .../com/conta/app/data/SettingsRepository.kt | 6 + app/src/main/java/com/conta/app/ui/AppRoot.kt | 3 + .../java/com/conta/app/ui/AppViewModel.kt | 6 + .../screens/categories/CategoryFormScreen.kt | 5 +- .../app/ui/screens/settings/SettingsScreen.kt | 10 + .../app/ui/screens/settings/ThemeScreen.kt | 147 +++++++++++ .../java/com/conta/app/ui/theme/AppThemes.kt | 246 ++++++++++++++++++ .../main/java/com/conta/app/ui/theme/Theme.kt | 164 +++++++----- .../com/conta/app/ui/util/CategoryColors.kt | 20 +- 10 files changed, 527 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/com/conta/app/ui/screens/settings/ThemeScreen.kt create mode 100644 app/src/main/java/com/conta/app/ui/theme/AppThemes.kt diff --git a/app/build.gradle b/app/build.gradle index d9e7bdb..658a4e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.conta.app" minSdk 24 targetSdk 34 - versionCode 4 - versionName "1.0.3" + versionCode 5 + versionName "1.0.4" } buildTypes { diff --git a/app/src/main/java/com/conta/app/data/SettingsRepository.kt b/app/src/main/java/com/conta/app/data/SettingsRepository.kt index 97b77b7..b992a86 100644 --- a/app/src/main/java/com/conta/app/data/SettingsRepository.kt +++ b/app/src/main/java/com/conta/app/data/SettingsRepository.kt @@ -13,11 +13,17 @@ class SettingsRepository(private val context: Context) { private object Keys { val BASE_URL = stringPreferencesKey("base_url") + val THEME_ID = stringPreferencesKey("theme_id") } val baseUrl: Flow = context.dataStore.data.map { it[Keys.BASE_URL] ?: "" } + val themeId: Flow = context.dataStore.data.map { it[Keys.THEME_ID] ?: "default" } suspend fun setBaseUrl(url: String) { context.dataStore.edit { it[Keys.BASE_URL] = url.trim() } } + + suspend fun setThemeId(id: String) { + context.dataStore.edit { it[Keys.THEME_ID] = id } + } } diff --git a/app/src/main/java/com/conta/app/ui/AppRoot.kt b/app/src/main/java/com/conta/app/ui/AppRoot.kt index c18a8bc..f2710b0 100644 --- a/app/src/main/java/com/conta/app/ui/AppRoot.kt +++ b/app/src/main/java/com/conta/app/ui/AppRoot.kt @@ -47,6 +47,7 @@ import com.conta.app.ui.screens.rules.RulesScreen import com.conta.app.ui.screens.settings.BackupScreen import com.conta.app.ui.screens.settings.NtfyScreen import com.conta.app.ui.screens.settings.SettingsScreen +import com.conta.app.ui.screens.settings.ThemeScreen import com.conta.app.ui.screens.transactions.TransactionFormScreen import com.conta.app.ui.screens.transactions.TransactionsScreen @@ -66,6 +67,7 @@ object Routes { const val SETTINGS = "settings" const val NTFY = "ntfy" const val BACKUP = "backup" + const val THEME = "theme" fun accountDetail(id: Long) = "$ACCOUNT_DETAIL/$id" fun accountForm(id: Long? = null) = @@ -151,6 +153,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel()) { composable(Routes.RECURRING) { RecurringScreen(viewModel, navController) } composable(Routes.NTFY) { NtfyScreen(viewModel, navController) } composable(Routes.BACKUP) { BackupScreen(viewModel, navController) } + composable(Routes.THEME) { ThemeScreen(viewModel, navController) } composable( route = "${Routes.ACCOUNT_DETAIL}/{accountId}", diff --git a/app/src/main/java/com/conta/app/ui/AppViewModel.kt b/app/src/main/java/com/conta/app/ui/AppViewModel.kt index 862c26d..7a7bd03 100644 --- a/app/src/main/java/com/conta/app/ui/AppViewModel.kt +++ b/app/src/main/java/com/conta/app/ui/AppViewModel.kt @@ -473,4 +473,10 @@ class AppViewModel : ViewModel() { refreshAll() } } + + fun setThemeId(id: String) { + viewModelScope.launch { + ServiceLocator.settings.setThemeId(id) + } + } } diff --git a/app/src/main/java/com/conta/app/ui/screens/categories/CategoryFormScreen.kt b/app/src/main/java/com/conta/app/ui/screens/categories/CategoryFormScreen.kt index 509cae4..c0cae65 100644 --- a/app/src/main/java/com/conta/app/ui/screens/categories/CategoryFormScreen.kt +++ b/app/src/main/java/com/conta/app/ui/screens/categories/CategoryFormScreen.kt @@ -50,7 +50,7 @@ import com.conta.app.data.model.CreateCategoryRequest import com.conta.app.data.model.UpdateCategoryRequest import com.conta.app.ui.AppViewModel import com.conta.app.ui.components.SectionHeader -import com.conta.app.ui.util.CATEGORY_COLOR_PALETTE +import com.conta.app.ui.util.categoryColorPalette import com.conta.app.ui.util.CATEGORY_ICON_NAMES import com.conta.app.ui.util.categoryIcon @@ -186,7 +186,8 @@ fun CategoryFormScreen(viewModel: AppViewModel, navController: NavController, ca modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - CATEGORY_COLOR_PALETTE.forEachIndexed { index, colorValue -> + val palette = categoryColorPalette() + palette.forEachIndexed { index, colorValue -> val selected = color == index.toString() Box( modifier = Modifier diff --git a/app/src/main/java/com/conta/app/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/conta/app/ui/screens/settings/SettingsScreen.kt index b724633..2957bb6 100644 --- a/app/src/main/java/com/conta/app/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/conta/app/ui/screens/settings/SettingsScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.Http import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Sync import androidx.compose.material3.Button @@ -125,6 +126,15 @@ fun SettingsScreen(viewModel: AppViewModel, navController: NavController) { onClick = { navController.navigate(Routes.NTFY) }, ) + androidx.compose.foundation.layout.Spacer(Modifier.padding(bottom = 20.dp)) + SectionHeader("Apariencia") + SettingsItemCard( + icon = Icons.Filled.Palette, + title = "Tema de colores", + subtitle = "Catppuccin, Gruvbox, Nord, Monokai y más", + onClick = { navController.navigate(Routes.THEME) }, + ) + androidx.compose.foundation.layout.Spacer(Modifier.padding(bottom = 20.dp)) SectionHeader("Datos") SettingsItemCard( diff --git a/app/src/main/java/com/conta/app/ui/screens/settings/ThemeScreen.kt b/app/src/main/java/com/conta/app/ui/screens/settings/ThemeScreen.kt new file mode 100644 index 0000000..5cc0d5c --- /dev/null +++ b/app/src/main/java/com/conta/app/ui/screens/settings/ThemeScreen.kt @@ -0,0 +1,147 @@ +package com.conta.app.ui.screens.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.conta.app.ServiceLocator +import com.conta.app.ui.AppViewModel +import com.conta.app.ui.components.SectionHeader +import com.conta.app.ui.theme.THEME_OPTIONS +import com.conta.app.ui.theme.ThemeSpec + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ThemeScreen(viewModel: AppViewModel, navController: NavController) { + val themeId by remember { ServiceLocator.settings.themeId }.collectAsStateWithLifecycle(initialValue = "default") + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Apariencia", fontWeight = FontWeight.Bold) }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver") + } + }, + ) + }, + contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp), + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + ) { + SectionHeader("Tema") + Text( + text = "Elige el tema de colores de la aplicación. El cambio se aplica de inmediato y se guarda en este dispositivo.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + androidx.compose.foundation.layout.Spacer(Modifier.padding(bottom = 12.dp)) + + THEME_OPTIONS.forEach { spec -> + ThemeRow( + spec = spec, + selected = themeId == spec.id, + onClick = { viewModel.setThemeId(spec.id) }, + ) + androidx.compose.foundation.layout.Spacer(Modifier.padding(bottom = 8.dp)) + } + androidx.compose.foundation.layout.Spacer(Modifier.padding(bottom = 16.dp)) + } + } +} + +@Composable +private fun ThemeRow(spec: ThemeSpec, selected: Boolean, onClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = if (selected) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.35f) + } else { + MaterialTheme.colorScheme.surface + }, + ), + ) { + Row( + modifier = Modifier.padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + ThemeDot(spec.primary) + ThemeDot(spec.secondary) + ThemeDot(spec.background, border = true) + } + Text( + text = spec.label, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f), + ) + if (selected) { + Icon( + Icons.Filled.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + } + } + } +} + +@Composable +private fun ThemeDot(color: Color, border: Boolean = false) { + Box( + modifier = Modifier + .size(16.dp) + .background(color, CircleShape) + .then( + if (border) Modifier.border(1.dp, MaterialTheme.colorScheme.onSurfaceVariant, CircleShape) + else Modifier, + ), + ) +} diff --git a/app/src/main/java/com/conta/app/ui/theme/AppThemes.kt b/app/src/main/java/com/conta/app/ui/theme/AppThemes.kt new file mode 100644 index 0000000..72fbf15 --- /dev/null +++ b/app/src/main/java/com/conta/app/ui/theme/AppThemes.kt @@ -0,0 +1,246 @@ +package com.conta.app.ui.theme + +import androidx.compose.ui.graphics.Color + +data class ThemeSpec( + val id: String, + val label: String, + val isDark: Boolean, + val primary: Color, + val secondary: Color, + val error: Color, + val warning: Color, + val success: Color, + val info: Color, + val background: Color, + val paper: Color, + val textPrimary: Color, + val textSecondary: Color, + val divider: Color, + val categoryColors: List, +) + +val THEME_OPTIONS: List = listOf( + ThemeSpec( + id = "default", + label = "Predeterminado", + isDark = false, + primary = Color(0xFF1565C0), + secondary = Color(0xFFE53935), + error = Color(0xFFD32F2F), + warning = Color(0xFFF57C00), + success = Color(0xFF2E7D32), + info = Color(0xFF0288D1), + background = Color(0xFFF5F5F5), + paper = Color(0xFFFFFFFF), + textPrimary = Color(0xDE000000), + textSecondary = Color(0x99000000), + divider = Color(0x1F000000), + categoryColors = listOf( + Color(0xFF1976D2), Color(0xFF7B1FA2), Color(0xFF00897B), Color(0xFFF9A825), + Color(0xFFE53935), Color(0xFF5D4037), Color(0xFF43A047), Color(0xFF6A1B9A), + Color(0xFFC2185B), Color(0xFF303F9F), Color(0xFF00ACC1), Color(0xFF7CB342), + Color(0xFFFFB300), Color(0xFFD81B60), + ), + ), + ThemeSpec( + id = "catppuccin-latte", + label = "Catppuccin Latte (claro)", + isDark = false, + primary = Color(0xFF1E66F5), + secondary = Color(0xFF8839EF), + error = Color(0xFFD20F39), + warning = Color(0xFFDF8E1D), + success = Color(0xFF40A02B), + info = Color(0xFF04A5E5), + background = Color(0xFFEFF1F5), + paper = Color(0xFFCCD0DA), + textPrimary = Color(0xFF4C4F69), + textSecondary = Color(0xFF6C6F85), + divider = Color(0xFFACB0BE), + categoryColors = listOf( + Color(0xFF1E66F5), Color(0xFF8839EF), Color(0xFF179299), Color(0xFFDF8E1D), + Color(0xFFD20F39), Color(0xFFFE640B), Color(0xFF40A02B), Color(0xFF7287FD), + Color(0xFFE64553), Color(0xFF04A5E5), Color(0xFFDD7878), Color(0xFFEA76CB), + Color(0xFF209FB5), Color(0xFF8C8FA1), + ), + ), + ThemeSpec( + id = "catppuccin-mocha", + label = "Catppuccin Mocha (oscuro)", + isDark = true, + primary = Color(0xFF89B4FA), + secondary = Color(0xFFCBA6F7), + error = Color(0xFFF38BA8), + warning = Color(0xFFF9E2AF), + success = Color(0xFFA6E3A1), + info = Color(0xFF89DCEB), + background = Color(0xFF1E1E2E), + paper = Color(0xFF313244), + textPrimary = Color(0xFFCDD6F4), + textSecondary = Color(0xFFA6ADC8), + divider = Color(0xFF585B70), + categoryColors = listOf( + Color(0xFF89B4FA), Color(0xFFCBA6F7), Color(0xFF94E2D5), Color(0xFFF9E2AF), + Color(0xFFF38BA8), Color(0xFFFAB387), Color(0xFFA6E3A1), Color(0xFFB4BEFE), + Color(0xFFEBA0AC), Color(0xFF89DCEB), Color(0xFFF2CDCD), Color(0xFFF5C2E7), + Color(0xFF74C7EC), Color(0xFFA6ADC8), + ), + ), + ThemeSpec( + id = "gruvbox-light", + label = "Gruvbox Claro", + isDark = false, + primary = Color(0xFF076678), + secondary = Color(0xFF8F3F71), + error = Color(0xFFCC241D), + warning = Color(0xFFB57614), + success = Color(0xFF79740E), + info = Color(0xFF427B58), + background = Color(0xFFFBF1C7), + paper = Color(0xFFEBDDB2), + textPrimary = Color(0xFF282828), + textSecondary = Color(0xFF928374), + divider = Color(0xFFD5C4A1), + categoryColors = listOf( + Color(0xFF076678), Color(0xFF8F3F71), Color(0xFF79740E), Color(0xFFB57614), + Color(0xFFCC241D), Color(0xFFAF3A03), Color(0xFF98971A), Color(0xFF427B58), + Color(0xFFB16286), Color(0xFF9D0006), Color(0xFF7C6F64), Color(0xFFD65D0E), + Color(0xFF689D6A), Color(0xFF504945), + ), + ), + ThemeSpec( + id = "gruvbox-dark", + label = "Gruvbox Oscuro", + isDark = true, + primary = Color(0xFF83A598), + secondary = Color(0xFFD3869B), + error = Color(0xFFFB4934), + warning = Color(0xFFFABD2F), + success = Color(0xFFB8BB26), + info = Color(0xFF8EC07C), + background = Color(0xFF282828), + paper = Color(0xFF3C3836), + textPrimary = Color(0xFFEBDDB2), + textSecondary = Color(0xFFBDAE93), + divider = Color(0xFF665C54), + categoryColors = listOf( + Color(0xFF83A598), Color(0xFFD3869B), Color(0xFF8EC07C), Color(0xFFFABD2F), + Color(0xFFFB4934), Color(0xFFD65D0E), Color(0xFFB8BB26), Color(0xFF458588), + Color(0xFFB16286), Color(0xFFCC241D), Color(0xFF98971A), Color(0xFF689D6A), + Color(0xFFA89984), Color(0xFFD79921), + ), + ), + ThemeSpec( + id = "everforest", + label = "Everforest", + isDark = true, + primary = Color(0xFF7FBBB3), + secondary = Color(0xFFD699B6), + error = Color(0xFFE67E80), + warning = Color(0xFFE69875), + success = Color(0xFFA7C080), + info = Color(0xFF83C092), + background = Color(0xFF2D353B), + paper = Color(0xFF343F44), + textPrimary = Color(0xFFD3C6AA), + textSecondary = Color(0xFF859289), + divider = Color(0xFF4F585E), + categoryColors = listOf( + Color(0xFF7FBBB3), Color(0xFFD699B6), Color(0xFFA7C080), Color(0xFFE69875), + Color(0xFFE67E80), Color(0xFFD3C6AA), Color(0xFF83C092), Color(0xFF899C97), + Color(0xFF6A916D), Color(0xFF9DA9A0), Color(0xFFA8B370), Color(0xFFD3869B), + Color(0xFF5C7C6A), Color(0xFFC4B28A), + ), + ), + ThemeSpec( + id = "nord", + label = "Nord", + isDark = true, + primary = Color(0xFF88C0D0), + secondary = Color(0xFFB48EAD), + error = Color(0xFFBF616A), + warning = Color(0xFFD08770), + success = Color(0xFFA3BE8C), + info = Color(0xFF81A1C1), + background = Color(0xFF2E3440), + paper = Color(0xFF3B4252), + textPrimary = Color(0xFFECEFF4), + textSecondary = Color(0xFFD8DEE9), + divider = Color(0xFF4C566A), + categoryColors = listOf( + Color(0xFF88C0D0), Color(0xFFB48EAD), Color(0xFFA3BE8C), Color(0xFFEBCB8B), + Color(0xFFBF616A), Color(0xFFD08770), Color(0xFF81A1C1), Color(0xFF8FBCBB), + Color(0xFF5E81AC), Color(0xFFD8DEE9), Color(0xFF4C566A), Color(0xFF63B3AD), + Color(0xFFC98076), Color(0xFF7EB4D8), + ), + ), + ThemeSpec( + id = "rose-pine-moon", + label = "Rosé Pine Moon", + isDark = true, + primary = Color(0xFF9CCFD8), + secondary = Color(0xFFC4A7E7), + error = Color(0xFFEB6F92), + warning = Color(0xFFF6C177), + success = Color(0xFF3E8FB0), + info = Color(0xFFEA9A97), + background = Color(0xFF232136), + paper = Color(0xFF2A273F), + textPrimary = Color(0xFFE0DEF4), + textSecondary = Color(0xFF908CAA), + divider = Color(0xFF393552), + categoryColors = listOf( + Color(0xFF9CCFD8), Color(0xFFC4A7E7), Color(0xFF3E8FB0), Color(0xFFEA9A97), + Color(0xFFEB6F92), Color(0xFFF6C177), Color(0xFF56949F), Color(0xFF908CAA), + Color(0xFF31748F), Color(0xFF907AA9), Color(0xFFE0DEF4), Color(0xFF44415A), + Color(0xFFD9D6E0), Color(0xFF2A273F), + ), + ), + ThemeSpec( + id = "monokai", + label = "Monokai", + isDark = true, + primary = Color(0xFF66D9EF), + secondary = Color(0xFFAE81FF), + error = Color(0xFFF92672), + warning = Color(0xFFFD971F), + success = Color(0xFFA6E22E), + info = Color(0xFFE6DB74), + background = Color(0xFF272822), + paper = Color(0xFF37372E), + textPrimary = Color(0xFFF8F8F2), + textSecondary = Color(0xFFCFCFC2), + divider = Color(0xFF49483E), + categoryColors = listOf( + Color(0xFF66D9EF), Color(0xFFAE81FF), Color(0xFFA6E22E), Color(0xFFE6DB74), + Color(0xFFF92672), Color(0xFFFD971F), Color(0xFFA1EFE4), Color(0xFFF8F8F2), + Color(0xFF75715E), Color(0xFFE69F66), Color(0xFFCC6633), Color(0xFFBD93F9), + Color(0xFF50FA7B), Color(0xFFFF79C6), + ), + ), + ThemeSpec( + id = "ayu-dark", + label = "Ayu Dark", + isDark = true, + primary = Color(0xFFFF8F40), + secondary = Color(0xFFD2A6FF), + error = Color(0xFFF07178), + warning = Color(0xFFFFB454), + success = Color(0xFFAAD94C), + info = Color(0xFF59C2FF), + background = Color(0xFF0B0E14), + paper = Color(0xFF141923), + textPrimary = Color(0xFFD9D7CE), + textSecondary = Color(0xFF626A73), + divider = Color(0xFF2B333E), + categoryColors = listOf( + Color(0xFFFF8F40), Color(0xFFD2A6FF), Color(0xFFAAD94C), Color(0xFFFFB454), + Color(0xFFF07178), Color(0xFF59C2FF), Color(0xFFE6B673), Color(0xFF95E6CB), + Color(0xFFF29E74), Color(0xFFD4BFFF), Color(0xFF73D0FF), Color(0xFFB8CC52), + Color(0xFFF7B2A1), Color(0xFF36A3D9), + ), + ), +) + +fun themeById(id: String): ThemeSpec = THEME_OPTIONS.firstOrNull { it.id == id } ?: THEME_OPTIONS.first() diff --git a/app/src/main/java/com/conta/app/ui/theme/Theme.kt b/app/src/main/java/com/conta/app/ui/theme/Theme.kt index 3d92496..42be73a 100644 --- a/app/src/main/java/com/conta/app/ui/theme/Theme.kt +++ b/app/src/main/java/com/conta/app/ui/theme/Theme.kt @@ -1,82 +1,106 @@ package com.conta.app.ui.theme -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.conta.app.ServiceLocator -val PrimaryLight = Color(0xFF1565C0) -val PrimaryDark = Color(0xFF90CAF9) -val Accent = Color(0xFF00796B) -val AccentDark = Color(0xFF80CBC4) -val Tertiary = Color(0xFF6A1B9A) -val TertiaryDark = Color(0xFFCE93D8) +/** Aplica el esquema de colores según el tema persistido en Ajustes. */ +@Composable +fun ContaTheme(content: @Composable () -> Unit) { + val themeId by ServiceLocator.settings.themeId.collectAsStateWithLifecycle(initialValue = "default") + val spec = themeById(themeId) + val colorScheme = if (spec.isDark) spec.toDarkColorScheme() else spec.toLightColorScheme() + CompositionLocalThemeSpec(spec) { + MaterialTheme( + colorScheme = colorScheme, + content = content, + ) + } +} -private val LightColors = lightColorScheme( - primary = PrimaryLight, - onPrimary = Color.White, - primaryContainer = Color(0xFFD6E3FF), - onPrimaryContainer = Color(0xFF0B2A56), - secondary = Accent, - onSecondary = Color.White, - secondaryContainer = Color(0xFFB2DFDB), - onSecondaryContainer = Color(0xFF00332E), - tertiary = Tertiary, - onTertiary = Color.White, - tertiaryContainer = Color(0xFFE1BEE7), - onTertiaryContainer = Color(0xFF310742), - error = Color(0xFFD32F2F), - onError = Color.White, - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFF5F6F8), - onBackground = Color(0xFF191C20), - surface = Color(0xFFFAFAFA), - onSurface = Color(0xFF191C20), - surfaceVariant = Color(0xFFE7E8EC), - onSurfaceVariant = Color(0xFF43474E), - outline = Color(0xFF73777F), - outlineVariant = Color(0xFFC4C6CC), - surfaceTint = PrimaryLight, -) - -private val DarkColors = darkColorScheme( - primary = PrimaryDark, - onPrimary = Color(0xFF0B2A56), - primaryContainer = Color(0xFF1E3A5F), - onPrimaryContainer = Color(0xFFD6E3FF), - secondary = AccentDark, - onSecondary = Color(0xFF00332E), - secondaryContainer = Color(0xFF00504A), - onSecondaryContainer = Color(0xFFB2DFDB), - tertiary = TertiaryDark, - onTertiary = Color(0xFF310742), - tertiaryContainer = Color(0xFF4A1584), - onTertiaryContainer = Color(0xFFE1BEE7), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF0F1216), - onBackground = Color(0xFFE2E2E6), - surface = Color(0xFF15181D), - onSurface = Color(0xFFE2E2E6), - surfaceVariant = Color(0xFF2A2E35), - onSurfaceVariant = Color(0xFFC4C6CC), - outline = Color(0xFF8E929A), - outlineVariant = Color(0xFF2A2E35), - surfaceTint = PrimaryDark, -) +/** CompositionLocal que expone el ThemeSpec activo (para colores de categorías, etc.). */ +val LocalThemeSpec = staticCompositionLocalOf { THEME_OPTIONS.first() } @Composable -fun ContaTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { - MaterialTheme( - colorScheme = if (darkTheme) DarkColors else LightColors, - content = content, - ) +private fun CompositionLocalThemeSpec(spec: ThemeSpec, content: @Composable () -> Unit) { + androidx.compose.runtime.CompositionLocalProvider(LocalThemeSpec provides spec, content = content) } + +private fun ThemeSpec.toLightColorScheme() = lightColorScheme( + primary = primary, + onPrimary = onColor(primary), + primaryContainer = lightContainer(primary), + onPrimaryContainer = onColor(lightContainer(primary)), + secondary = secondary, + onSecondary = onColor(secondary), + secondaryContainer = lightContainer(secondary), + onSecondaryContainer = onColor(lightContainer(secondary)), + tertiary = info, + onTertiary = onColor(info), + tertiaryContainer = lightContainer(info), + onTertiaryContainer = onColor(lightContainer(info)), + error = error, + onError = onColor(error), + errorContainer = lightContainer(error), + onErrorContainer = onColor(lightContainer(error)), + background = background, + onBackground = textPrimary, + surface = paper, + onSurface = textPrimary, + surfaceVariant = divider, + onSurfaceVariant = textSecondary, + outline = textSecondary, + outlineVariant = divider, + surfaceTint = primary, +) + +private fun ThemeSpec.toDarkColorScheme() = darkColorScheme( + primary = primary, + onPrimary = onColor(primary), + primaryContainer = darkContainer(primary), + onPrimaryContainer = onColor(darkContainer(primary)), + secondary = secondary, + onSecondary = onColor(secondary), + secondaryContainer = darkContainer(secondary), + onSecondaryContainer = onColor(darkContainer(secondary)), + tertiary = info, + onTertiary = onColor(info), + tertiaryContainer = darkContainer(info), + onTertiaryContainer = onColor(darkContainer(info)), + error = error, + onError = onColor(error), + errorContainer = darkContainer(error), + onErrorContainer = onColor(darkContainer(error)), + background = background, + onBackground = textPrimary, + surface = paper, + onSurface = textPrimary, + surfaceVariant = divider, + onSurfaceVariant = textSecondary, + outline = textSecondary, + outlineVariant = divider, + surfaceTint = primary, +) + +/** Color de contraste (blanco/negro) para texto sobre un fondo dado. */ +private fun onColor(bg: Color): Color = if (bg.luminance() > 0.5f) Color.Black else Color.White + +private fun Color.luminance(): Float = + 0.2126f * red + 0.7152f * green + 0.0722f * blue + +private fun lightContainer(base: Color): Color = base.blend(Color.White, t = 0.75f) + +private fun darkContainer(base: Color): Color = base.blend(Color.Black, t = 0.65f) + +private fun Color.blend(other: Color, t: Float = 0.7f): Color = Color( + red = red * (1 - t) + other.red * t, + green = green * (1 - t) + other.green * t, + blue = blue * (1 - t) + other.blue * t, + alpha = 1f, +) diff --git a/app/src/main/java/com/conta/app/ui/util/CategoryColors.kt b/app/src/main/java/com/conta/app/ui/util/CategoryColors.kt index 902cd29..330bb31 100644 --- a/app/src/main/java/com/conta/app/ui/util/CategoryColors.kt +++ b/app/src/main/java/com/conta/app/ui/util/CategoryColors.kt @@ -1,21 +1,21 @@ package com.conta.app.ui.util +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import com.conta.app.ui.theme.LocalThemeSpec -/** Paleta por defecto para categorías (misma que el frontend). */ -val CATEGORY_COLOR_PALETTE = listOf( - Color(0xFF1976D2), Color(0xFF7B1FA2), Color(0xFF00897B), Color(0xFFF9A825), - Color(0xFFE53935), Color(0xFF5D4037), Color(0xFF43A047), Color(0xFF6A1B9A), - Color(0xFFC2185B), Color(0xFF303F9F), Color(0xFF00ACC1), Color(0xFF7CB342), - Color(0xFFFFB300), Color(0xFFD81B60), -) +/** Paleta de colores de categorías según el tema activo (misma que el frontend). */ +@Composable +fun categoryColorPalette(): List = LocalThemeSpec.current.categoryColors +@Composable fun resolveCategoryColor(color: String?, fallback: Color): Color { if (color.isNullOrBlank()) return fallback - // Si es un índice de la paleta (0-13), se resuelve contra la paleta. + val palette = categoryColorPalette() + // Si es un índice de la paleta (0-13), se resuelve contra la paleta del tema. val idx = color.toIntOrNull() - if (idx != null && idx in 0 until CATEGORY_COLOR_PALETTE.size) { - return CATEGORY_COLOR_PALETTE[idx] + if (idx != null && idx in 0 until palette.size) { + return palette[idx] } // Si es un color hex (##RRGGBB o #RRGGBB). val hex = color.trim().removePrefix("##").removePrefix("#")