Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6bf1ecbf2 | ||
|
|
b1e2a13a5f | ||
|
|
1d0228fae0 |
+2
-2
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.conta.app"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 2
|
||||
versionName "1.0.1"
|
||||
versionCode 5
|
||||
versionName "1.0.4"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -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<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) {
|
||||
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.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}",
|
||||
|
||||
@@ -473,4 +473,10 @@ class AppViewModel : ViewModel() {
|
||||
refreshAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun setThemeId(id: String) {
|
||||
viewModelScope.launch {
|
||||
ServiceLocator.settings.setThemeId(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.layout.WindowInsets
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -20,7 +21,6 @@ import androidx.compose.material.icons.filled.AccountBalance
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.material3.Icon
|
||||
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.ui.Alignment
|
||||
@@ -68,16 +67,7 @@ fun AccountsScreen(viewModel: AppViewModel, navController: NavController) {
|
||||
val loading by viewModel.loading.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Cuentas", fontWeight = FontWeight.Bold) },
|
||||
actions = {
|
||||
androidx.compose.material3.IconButton(onClick = { viewModel.refreshAccounts() }) {
|
||||
Icon(Icons.Filled.Sync, contentDescription = "Actualizar")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { navController.navigate(Routes.accountForm()) }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Nueva cuenta")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.layout.WindowInsets
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -21,7 +22,6 @@ import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
import androidx.compose.material.icons.filled.NotificationsActive
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.icons.filled.Savings
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.Card
|
||||
@@ -33,7 +33,6 @@ 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.ui.Alignment
|
||||
@@ -68,16 +67,7 @@ fun DashboardScreen(viewModel: AppViewModel, navController: NavController) {
|
||||
val loading by viewModel.loading.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Conta", fontWeight = FontWeight.Bold) },
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.refreshDashboard() }) {
|
||||
Icon(Icons.Filled.Sync, contentDescription = "Actualizar")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { navController.navigate(Routes.transactionForm(preset = "expense")) }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Nueva transacción")
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -14,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
|
||||
@@ -26,7 +28,6 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -63,11 +64,7 @@ fun SettingsScreen(viewModel: AppViewModel, navController: NavController) {
|
||||
val summary by viewModel.summary.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Ajustes", fontWeight = FontWeight.Bold) },
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -129,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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -5,23 +5,21 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
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.mutableStateOf
|
||||
@@ -56,16 +54,7 @@ fun TransactionsScreen(viewModel: AppViewModel, navController: NavController) {
|
||||
val totalExpense = filtered.filter { it.transactionType == "expense" }.sumOf { it.amount }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Movimientos", fontWeight = FontWeight.Bold) },
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.refreshDashboard() }) {
|
||||
Icon(Icons.Filled.Sync, contentDescription = "Actualizar")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { navController.navigate(Routes.transactionForm()) }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Nueva transacción")
|
||||
|
||||
@@ -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
|
||||
|
||||
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<ThemeSpec> { 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,
|
||||
)
|
||||
|
||||
@@ -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<Color> = 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("#")
|
||||
|
||||
Reference in New Issue
Block a user