1 Commits
10 changed files with 527 additions and 84 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.conta.app" applicationId "com.conta.app"
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
versionCode 4 versionCode 5
versionName "1.0.3" versionName "1.0.4"
} }
buildTypes { buildTypes {
@@ -13,11 +13,17 @@ class SettingsRepository(private val context: Context) {
private object Keys { private object Keys {
val BASE_URL = stringPreferencesKey("base_url") val BASE_URL = stringPreferencesKey("base_url")
val THEME_ID = stringPreferencesKey("theme_id")
} }
val baseUrl: Flow<String> = context.dataStore.data.map { it[Keys.BASE_URL] ?: "" } val baseUrl: Flow<String> = context.dataStore.data.map { it[Keys.BASE_URL] ?: "" }
val themeId: Flow<String> = context.dataStore.data.map { it[Keys.THEME_ID] ?: "default" }
suspend fun setBaseUrl(url: String) { suspend fun setBaseUrl(url: String) {
context.dataStore.edit { it[Keys.BASE_URL] = url.trim() } context.dataStore.edit { it[Keys.BASE_URL] = url.trim() }
} }
suspend fun setThemeId(id: String) {
context.dataStore.edit { it[Keys.THEME_ID] = id }
}
} }
@@ -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.BackupScreen
import com.conta.app.ui.screens.settings.NtfyScreen import com.conta.app.ui.screens.settings.NtfyScreen
import com.conta.app.ui.screens.settings.SettingsScreen 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.TransactionFormScreen
import com.conta.app.ui.screens.transactions.TransactionsScreen import com.conta.app.ui.screens.transactions.TransactionsScreen
@@ -66,6 +67,7 @@ object Routes {
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val NTFY = "ntfy" const val NTFY = "ntfy"
const val BACKUP = "backup" const val BACKUP = "backup"
const val THEME = "theme"
fun accountDetail(id: Long) = "$ACCOUNT_DETAIL/$id" fun accountDetail(id: Long) = "$ACCOUNT_DETAIL/$id"
fun accountForm(id: Long? = null) = fun accountForm(id: Long? = null) =
@@ -151,6 +153,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel()) {
composable(Routes.RECURRING) { RecurringScreen(viewModel, navController) } composable(Routes.RECURRING) { RecurringScreen(viewModel, navController) }
composable(Routes.NTFY) { NtfyScreen(viewModel, navController) } composable(Routes.NTFY) { NtfyScreen(viewModel, navController) }
composable(Routes.BACKUP) { BackupScreen(viewModel, navController) } composable(Routes.BACKUP) { BackupScreen(viewModel, navController) }
composable(Routes.THEME) { ThemeScreen(viewModel, navController) }
composable( composable(
route = "${Routes.ACCOUNT_DETAIL}/{accountId}", route = "${Routes.ACCOUNT_DETAIL}/{accountId}",
@@ -473,4 +473,10 @@ class AppViewModel : ViewModel() {
refreshAll() refreshAll()
} }
} }
fun setThemeId(id: String) {
viewModelScope.launch {
ServiceLocator.settings.setThemeId(id)
}
}
} }
@@ -50,7 +50,7 @@ import com.conta.app.data.model.CreateCategoryRequest
import com.conta.app.data.model.UpdateCategoryRequest import com.conta.app.data.model.UpdateCategoryRequest
import com.conta.app.ui.AppViewModel import com.conta.app.ui.AppViewModel
import com.conta.app.ui.components.SectionHeader 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.CATEGORY_ICON_NAMES
import com.conta.app.ui.util.categoryIcon import com.conta.app.ui.util.categoryIcon
@@ -186,7 +186,8 @@ fun CategoryFormScreen(viewModel: AppViewModel, navController: NavController, ca
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
CATEGORY_COLOR_PALETTE.forEachIndexed { index, colorValue -> val palette = categoryColorPalette()
palette.forEachIndexed { index, colorValue ->
val selected = color == index.toString() val selected = color == index.toString()
Box( Box(
modifier = Modifier modifier = Modifier
@@ -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.Cloud
import androidx.compose.material.icons.filled.Http import androidx.compose.material.icons.filled.Http
import androidx.compose.material.icons.filled.NotificationsActive 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.Save
import androidx.compose.material.icons.filled.Sync import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -125,6 +126,15 @@ fun SettingsScreen(viewModel: AppViewModel, navController: NavController) {
onClick = { navController.navigate(Routes.NTFY) }, 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)) androidx.compose.foundation.layout.Spacer(Modifier.padding(bottom = 20.dp))
SectionHeader("Datos") SectionHeader("Datos")
SettingsItemCard( SettingsItemCard(
@@ -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,
),
)
}
@@ -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<Color>,
)
val THEME_OPTIONS: List<ThemeSpec> = 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()
@@ -1,82 +1,106 @@
package com.conta.app.ui.theme package com.conta.app.ui.theme
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
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.conta.app.ServiceLocator
val PrimaryLight = Color(0xFF1565C0) /** Aplica el esquema de colores según el tema persistido en Ajustes. */
val PrimaryDark = Color(0xFF90CAF9) @Composable
val Accent = Color(0xFF00796B) fun ContaTheme(content: @Composable () -> Unit) {
val AccentDark = Color(0xFF80CBC4) val themeId by ServiceLocator.settings.themeId.collectAsStateWithLifecycle(initialValue = "default")
val Tertiary = Color(0xFF6A1B9A) val spec = themeById(themeId)
val TertiaryDark = Color(0xFFCE93D8) val colorScheme = if (spec.isDark) spec.toDarkColorScheme() else spec.toLightColorScheme()
CompositionLocalThemeSpec(spec) {
MaterialTheme(
colorScheme = colorScheme,
content = content,
)
}
}
private val LightColors = lightColorScheme( /** CompositionLocal que expone el ThemeSpec activo (para colores de categorías, etc.). */
primary = PrimaryLight, val LocalThemeSpec = staticCompositionLocalOf<ThemeSpec> { THEME_OPTIONS.first() }
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,
)
@Composable @Composable
fun ContaTheme( private fun CompositionLocalThemeSpec(spec: ThemeSpec, content: @Composable () -> Unit) {
darkTheme: Boolean = isSystemInDarkTheme(), androidx.compose.runtime.CompositionLocalProvider(LocalThemeSpec provides spec, content = content)
content: @Composable () -> Unit,
) {
MaterialTheme(
colorScheme = if (darkTheme) DarkColors else LightColors,
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,
)
@@ -1,21 +1,21 @@
package com.conta.app.ui.util package com.conta.app.ui.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.conta.app.ui.theme.LocalThemeSpec
/** Paleta por defecto para categorías (misma que el frontend). */ /** Paleta de colores de categorías según el tema activo (misma que el frontend). */
val CATEGORY_COLOR_PALETTE = listOf( @Composable
Color(0xFF1976D2), Color(0xFF7B1FA2), Color(0xFF00897B), Color(0xFFF9A825), fun categoryColorPalette(): List<Color> = LocalThemeSpec.current.categoryColors
Color(0xFFE53935), Color(0xFF5D4037), Color(0xFF43A047), Color(0xFF6A1B9A),
Color(0xFFC2185B), Color(0xFF303F9F), Color(0xFF00ACC1), Color(0xFF7CB342),
Color(0xFFFFB300), Color(0xFFD81B60),
)
@Composable
fun resolveCategoryColor(color: String?, fallback: Color): Color { fun resolveCategoryColor(color: String?, fallback: Color): Color {
if (color.isNullOrBlank()) return fallback 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() val idx = color.toIntOrNull()
if (idx != null && idx in 0 until CATEGORY_COLOR_PALETTE.size) { if (idx != null && idx in 0 until palette.size) {
return CATEGORY_COLOR_PALETTE[idx] return palette[idx]
} }
// Si es un color hex (##RRGGBB o #RRGGBB). // Si es un color hex (##RRGGBB o #RRGGBB).
val hex = color.trim().removePrefix("##").removePrefix("#") val hex = color.trim().removePrefix("##").removePrefix("#")