Gasolinera app

This commit is contained in:
2026-08-14 13:58:02 +02:00
parent 087053c959
commit 4655f0008c
9 changed files with 753 additions and 101 deletions
+50
View File
@@ -0,0 +1,50 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>
+10 -8
View File
@@ -3,15 +3,13 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-feature
android:name="android.hardware.type.automotive"
android:required="false" />
<uses-feature
android:name="android.software.car.templates_host"
android:required="false" />
android:required="true" />
<application
android:allowBackup="true"
@@ -23,12 +21,12 @@
android:supportsRtl="true"
android:theme="@style/Theme.Prueba">
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1" />
<meta-data
android:name="androidx.car.app.category"
android:value="poi" />
<activity
android:name=".MainActivity"
@@ -38,15 +36,19 @@
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Indicar al sistema que la actividad está optimizada para el coche -->
<meta-data
android:name="distractionOptimized"
android:value="true" />
</activity>
<service
android:name=".MyCarAppService"
android:exported="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:permission="androidx.car.app.PERMISSION_CAR_APP">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
@@ -1,6 +1,7 @@
package com.example.prueba
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -10,10 +11,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.AltRoute
import androidx.compose.material.icons.filled.*
@@ -23,7 +26,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -37,14 +42,13 @@ import com.example.prueba.data.PriceCategory
import com.example.prueba.ui.GasStationViewModel
import com.example.prueba.ui.theme.PruebaTheme
import com.example.prueba.utils.FuelRecord
import android.content.Context
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
import android.preference.PreferenceManager
import java.io.File
import java.util.Locale
class MainActivity : ComponentActivity() {
@@ -52,8 +56,9 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
// OSMDroid configuration
Configuration.getInstance().load(this, getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
Configuration.getInstance().userAgentValue = "GasolinerasApp/1.0 (Android; Contact: support@example.com)"
val osmConfig = Configuration.getInstance()
osmConfig.userAgentValue = "Mozilla/5.0 (Android) GasolinerasApp/1.0"
osmConfig.load(this, getSharedPreferences("osmdroid_prefs", MODE_PRIVATE))
enableEdgeToEdge()
setContent {
@@ -73,6 +78,8 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
var showFuelDialog by remember { mutableStateOf<GasStationInfo?>(null) }
var editRecordDialog by remember { mutableStateOf<FuelRecord?>(null) }
var selectedStationForMap by remember { mutableStateOf<GasStationInfo?>(null) }
var showHistoryFilter by remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
@@ -148,14 +155,36 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
}
},
actions = {
if (!uiState.isRouteActive && uiState.selectedTab == 0) {
// Filtro de favoritos (solo en lista o mapa)
if (uiState.selectedTab == 0 || uiState.selectedTab == 2) {
IconButton(onClick = { viewModel.toggleFavoritesFilter() }) {
Icon(
imageVector = if (uiState.showOnlyFavorites) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = stringResource(R.string.show_favorites),
tint = if (uiState.showOnlyFavorites) Color.Red else LocalContentColor.current
)
}
}
// Filtro de historial
if (uiState.selectedTab == 1) {
IconButton(onClick = { showHistoryFilter = true }) {
Icon(
imageVector = if (uiState.filterMonth != null || uiState.filterYear != null) Icons.Default.FilterList else Icons.Default.FilterListOff,
contentDescription = stringResource(R.string.filter_history),
tint = if (uiState.filterMonth != null || uiState.filterYear != null) MaterialTheme.colorScheme.primary else LocalContentColor.current
)
}
}
if (!uiState.isRouteActive && (uiState.selectedTab == 0 || uiState.selectedTab == 2)) {
IconButton(onClick = { viewModel.setRouteActive(true) }) {
Icon(Icons.AutoMirrored.Filled.AltRoute, stringResource(R.string.on_route))
}
}
}
)
if (uiState.selectedTab == 0) {
if (uiState.selectedTab == 0 || uiState.selectedTab == 2) {
FuelTypeSelector(
selectedType = uiState.selectedFuelType,
onTypeSelected = { viewModel.selectFuelType(it) }
@@ -205,31 +234,115 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
} else if (uiState.stations.isEmpty()) {
Text(stringResource(R.string.no_results), modifier = Modifier.align(Alignment.Center))
} else {
GasStationList(
stations = uiState.stations,
visibleCount = uiState.visibleStationsCount,
selectedFuelType = uiState.selectedFuelType,
onLogClick = { showFuelDialog = it },
onShowMore = { viewModel.showMoreStations() },
onShowLess = { viewModel.resetStationLimit() }
)
GasStationList(
stations = uiState.stations,
visibleCount = uiState.visibleStationsCount,
selectedFuelType = uiState.selectedFuelType,
favoriteIds = uiState.favoriteIds,
onLogClick = { showFuelDialog = it },
onShowMore = { viewModel.showMoreStations() },
onShowLess = { viewModel.resetStationLimit() },
onToggleFavorite = { viewModel.toggleFavorite(it) }
)
}
}
}
1 -> {
FuelHistoryScreen(
records = uiState.fuelRecords,
filterMonth = uiState.filterMonth,
filterYear = uiState.filterYear,
onDelete = { record -> viewModel.deleteFuelRecord(record) },
onEdit = { record -> editRecordDialog = record }
)
}
2 -> {
GasStationMap(
stations = uiState.stations,
routePoints = uiState.routePoints,
userLocation = uiState.lastLocation,
selectedFuelType = uiState.selectedFuelType
)
Box(modifier = Modifier.fillMaxSize()) {
GasStationMap(
stations = uiState.stations,
routePoints = uiState.routePoints,
userLocation = uiState.lastLocation,
selectedFuelType = uiState.selectedFuelType,
favoriteIds = uiState.favoriteIds,
onMapMoved = { lat, lon -> viewModel.updateMapCenter(lat, lon) },
onStationSelected = { selectedStationForMap = it }
)
// Botón flotante para buscar en la zona del mapa
Button(
onClick = { viewModel.searchAtMapCenter() },
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 16.dp),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 8.dp)
) {
Icon(Icons.Default.Search, null)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.search_this_area))
}
// Tarjeta informativa al seleccionar una gasolinera
selectedStationForMap?.let { station ->
Card(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(station.name, style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f))
IconButton(onClick = { viewModel.toggleFavorite(station.id) }) {
Icon(
imageVector = if (uiState.favoriteIds.contains(station.id)) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = null,
tint = if (uiState.favoriteIds.contains(station.id)) Color.Red else LocalContentColor.current
)
}
}
Text(station.address, style = MaterialTheme.typography.bodySmall)
val price = station.getPriceFor(uiState.selectedFuelType)
Text(
text = "${uiState.selectedFuelType}: $price €/L",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
}
IconButton(onClick = { selectedStationForMap = null }) {
Icon(Icons.Default.Close, null)
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
val uri = "waze://?ll=${station.latitude},${station.longitude}&navigate=yes"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
try {
context.startActivity(intent)
} catch (e: Exception) {
// Fallback a Google Maps si Waze no está instalado
val gmapsUri = "google.navigation:q=${station.latitude},${station.longitude}"
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(gmapsUri)))
}
},
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Directions, null)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.go_to_waze))
}
}
}
}
}
}
}
}
@@ -258,16 +371,17 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
FuelType.BIODIESEL -> stringResource(R.string.biodiesel_label)
},
onDismiss = { showFuelDialog = null },
onConfirm = { liters, total, type, pricePerLiter ->
onConfirm = { liters, total, type, pricePerLiter, date, odo ->
val record = FuelRecord(
date = System.currentTimeMillis(),
date = date,
stationName = station.name,
address = station.address,
municipality = station.municipality,
liters = liters,
totalPrice = total,
pricePerLiter = pricePerLiter,
fuelType = type
fuelType = type,
odometer = odo
)
viewModel.saveFuelRecord(record)
showFuelDialog = null
@@ -275,22 +389,41 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
)
}
editRecordDialog?.let { record ->
FuelLogDialog(
stationName = record.stationName,
initialLiters = record.liters.toString(),
initialTotal = record.totalPrice.toString(),
initialType = record.fuelType,
onDismiss = { editRecordDialog = null },
onConfirm = { liters, total, type, pricePerLiter ->
val newRecord = record.copy(
liters = liters,
totalPrice = total,
fuelType = type,
pricePerLiter = pricePerLiter
)
viewModel.updateFuelRecord(record, newRecord)
editRecordDialog = null
if (editRecordDialog != null) {
editRecordDialog?.let { record ->
FuelLogDialog(
stationName = record.stationName,
initialLiters = record.liters.toString(),
initialTotal = record.totalPrice.toString(),
initialType = record.fuelType,
initialDate = record.date,
initialOdometer = record.odometer?.toString() ?: "",
onDismiss = { editRecordDialog = null },
onConfirm = { liters, total, type, pricePerLiter, date, odo ->
val newRecord = record.copy(
date = date,
liters = liters,
totalPrice = total,
fuelType = type,
pricePerLiter = pricePerLiter,
odometer = odo
)
viewModel.updateFuelRecord(record, newRecord)
editRecordDialog = null
}
)
}
}
if (showHistoryFilter) {
HistoryFilterDialog(
records = uiState.fuelRecords,
currentMonth = uiState.filterMonth,
currentYear = uiState.filterYear,
onDismiss = { showHistoryFilter = false },
onConfirm = { m, y ->
viewModel.setHistoryFilters(m, y)
showHistoryFilter = false
}
)
}
@@ -303,23 +436,52 @@ fun GasStationMap(
stations: List<GasStationInfo>,
routePoints: List<android.location.Location>,
userLocation: android.location.Location?,
selectedFuelType: FuelType
selectedFuelType: FuelType,
favoriteIds: Set<String>,
onMapMoved: (Double, Double) -> Unit,
onStationSelected: (GasStationInfo) -> Unit
) {
AndroidView(
factory = { context ->
MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK)
setTileSource(org.osmdroid.tileprovider.tilesource.XYTileSource(
"CartoVoyager", 0, 20, 256, ".png",
arrayOf("https://a.basemaps.cartocdn.com/rastertiles/voyager/",
"https://b.basemaps.cartocdn.com/rastertiles/voyager/",
"https://c.basemaps.cartocdn.com/rastertiles/voyager/")
))
setMultiTouchControls(true)
setLayerType(android.view.View.LAYER_TYPE_SOFTWARE, null)
controller.setZoom(15.0)
userLocation?.let {
controller.setCenter(GeoPoint(it.latitude, it.longitude))
}
// Detectar movimiento del mapa
addMapListener(object : org.osmdroid.events.MapListener {
override fun onScroll(event: org.osmdroid.events.ScrollEvent?): Boolean {
onMapMoved(mapCenter.latitude, mapCenter.longitude)
return true
}
override fun onZoom(event: org.osmdroid.events.ZoomEvent?): Boolean {
onMapMoved(mapCenter.latitude, mapCenter.longitude)
return true
}
})
}
},
update = { mapView ->
mapView.overlays.clear()
// 1. Trazar la ruta (Polilínea)
// 1. Centrar en el usuario si no hay ruta y es la primera vez que entra al mapa
if (routePoints.isEmpty() && userLocation != null && mapView.tag == null) {
mapView.controller.setCenter(GeoPoint(userLocation.latitude, userLocation.longitude))
mapView.controller.setZoom(15.0)
mapView.tag = "centered"
}
// 2. Trazar la ruta (Polilínea)
if (routePoints.isNotEmpty()) {
val line = Polyline(mapView)
line.setPoints(routePoints.map { GeoPoint(it.latitude, it.longitude) })
@@ -327,33 +489,60 @@ fun GasStationMap(
line.outlinePaint.strokeWidth = 10f
mapView.overlays.add(line)
// Zoom para ajustar a la ruta la primera vez
if (mapView.tag == null) {
// Zoom para ajustar a la ruta si es nueva
if (mapView.tag != "route_zoomed") {
val points = routePoints.map { GeoPoint(it.latitude, it.longitude) }
mapView.zoomToBoundingBox(org.osmdroid.util.BoundingBox.fromGeoPoints(points), true)
mapView.tag = "zoomed"
if (points.isNotEmpty()) {
mapView.zoomToBoundingBox(org.osmdroid.util.BoundingBox.fromGeoPoints(points), true)
mapView.tag = "route_zoomed"
}
}
} else if (mapView.tag == "route_zoomed") {
// Si se desactivó la ruta, permitir volver a centrar en el usuario
mapView.tag = null
}
// 2. Marcador de usuario
// 3. Marcador de usuario
userLocation?.let {
val userMarker = Marker(mapView)
userMarker.position = GeoPoint(it.latitude, it.longitude)
userMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
userMarker.title = "Tu ubicación"
userMarker.icon = ContextCompat.getDrawable(mapView.context, org.osmdroid.library.R.drawable.person)
mapView.overlays.add(userMarker)
}
// 3. Marcadores de gasolineras
stations.forEach { station ->
val marker = Marker(mapView)
marker.position = GeoPoint(station.latitude, station.longitude)
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
val price = station.getPriceFor(selectedFuelType) ?: 0.0
// 4. Marcadores de gasolineras con colores
stations.forEach { station ->
val marker = Marker(mapView)
marker.position = GeoPoint(station.latitude, station.longitude)
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
val price = station.getPriceFor(selectedFuelType) ?: 0.0
marker.title = "${station.name}: $price €/L"
marker.subDescription = station.address
val isFavorite = favoriteIds.contains(station.id)
marker.setOnMarkerClickListener { m, _ ->
onStationSelected(station)
m.showInfoWindow()
true
}
val iconRes = org.osmdroid.library.R.drawable.marker_default
val drawable = ContextCompat.getDrawable(mapView.context, iconRes)?.mutate()
drawable?.let {
val color = when {
isFavorite -> android.graphics.Color.MAGENTA // Color especial para favoritos
station.priceCategory == PriceCategory.CHEAP -> android.graphics.Color.rgb(46, 125, 50) // Verde
station.priceCategory == PriceCategory.EXPENSIVE -> android.graphics.Color.rgb(198, 40, 40) // Rojo
else -> android.graphics.Color.rgb(251, 192, 45) // Amarillo
}
it.setTint(color)
marker.icon = it
}
mapView.overlays.add(marker)
}
@@ -405,9 +594,11 @@ fun GasStationList(
stations: List<GasStationInfo>,
visibleCount: Int,
selectedFuelType: FuelType,
favoriteIds: Set<String>,
onLogClick: (GasStationInfo) -> Unit,
onShowMore: () -> Unit,
onShowLess: () -> Unit
onShowLess: () -> Unit,
onToggleFavorite: (String) -> Unit
) {
val context = LocalContext.current
val visibleStations = stations.take(visibleCount)
@@ -417,11 +608,13 @@ fun GasStationList(
GasStationItem(
station = station,
selectedFuelType = selectedFuelType,
isFavorite = favoriteIds.contains(station.id),
onClick = {
val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}(${Uri.encode(station.name)})")
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
},
onLogClick = { onLogClick(station) }
onLogClick = { onLogClick(station) },
onToggleFavorite = { onToggleFavorite(station.id) }
)
}
@@ -454,17 +647,133 @@ fun GasStationList(
}
@Composable
fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit, onEdit: (FuelRecord) -> Unit) {
if (records.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(R.string.no_records)) }
} else {
LazyColumn(contentPadding = PaddingValues(16.dp)) {
items(records) { record ->
fun FuelHistoryScreen(
records: List<FuelRecord>,
filterMonth: Int?,
filterYear: Int?,
onDelete: (FuelRecord) -> Unit,
onEdit: (FuelRecord) -> Unit
) {
val context = LocalContext.current
val calInstance = java.util.Calendar.getInstance()
val nowMonth = calInstance.get(java.util.Calendar.MONTH)
val nowYear = calInstance.get(java.util.Calendar.YEAR)
val allRecordsSorted = records.sortedByDescending { it.date }
val filteredRecords = allRecordsSorted.filter {
val cal = java.util.Calendar.getInstance().apply { timeInMillis = it.date }
val mMatch = filterMonth == null || cal.get(java.util.Calendar.MONTH) == filterMonth
val yMatch = filterYear == null || cal.get(java.util.Calendar.YEAR) == filterYear
mMatch && yMatch
}
val displayMonth = filterMonth ?: nowMonth
val displayYear = filterYear ?: nowYear
val monthlyTotal = records.filter {
val cal = java.util.Calendar.getInstance().apply { timeInMillis = it.date }
cal.get(java.util.Calendar.MONTH) == displayMonth && cal.get(java.util.Calendar.YEAR) == displayYear
}.sumOf { it.totalPrice }
val yearlyTotal = records.filter {
val cal = java.util.Calendar.getInstance().apply { timeInMillis = it.date }
cal.get(java.util.Calendar.YEAR) == displayYear
}.sumOf { it.totalPrice }
LazyColumn(contentPadding = PaddingValues(16.dp)) {
item {
Column {
if (filterMonth != null || filterYear != null) {
val monthResId = if (filterMonth != null) context.resources.getIdentifier("month_$filterMonth", "string", context.packageName) else 0
val monthName = if (monthResId != 0) stringResource(monthResId) else stringResource(R.string.all_months)
val yearName = filterYear?.toString() ?: stringResource(R.string.all_years)
Text(
text = "Viendo: $monthName $yearName",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp)
)
}
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Card(
modifier = Modifier.weight(1f),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(stringResource(R.string.monthly_spend), style = MaterialTheme.typography.labelMedium)
Text("${String.format(Locale.getDefault(), "%.2f", monthlyTotal)}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
}
Card(
modifier = Modifier.weight(1f),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(stringResource(R.string.yearly_spend), style = MaterialTheme.typography.labelMedium)
Text("${String.format(Locale.getDefault(), "%.2f", yearlyTotal)}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
}
}
}
}
if (filteredRecords.isEmpty()) {
item {
Box(modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.no_records))
}
}
} else {
items(filteredRecords) { record ->
// Buscar el registro inmediatamente anterior en la lista completa (cronológicamente)
val fullIndex = allRecordsSorted.indexOf(record)
var consumption: Double? = null
if (record.odometer != null && fullIndex != -1 && fullIndex < allRecordsSorted.size - 1) {
val prevRecord = allRecordsSorted[fullIndex + 1]
if (prevRecord.odometer != null && record.odometer > prevRecord.odometer) {
val kmDiff = record.odometer - prevRecord.odometer
consumption = (record.liters / kmDiff) * 100
}
}
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(modifier = Modifier.weight(1f)) {
Text(record.stationName, style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
val isRepsol = record.stationName.contains("REPSOL", ignoreCase = true)
val (bColor, bInitial) = when {
isRepsol -> Color(0xFFE30613) to "R"
record.stationName.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000) to "C"
record.stationName.contains("BP", ignoreCase = true) -> Color(0xFF00A94F) to "B"
record.stationName.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00) to "G"
record.stationName.contains("SHELL", ignoreCase = true) -> Color(0xFFFFD500) to "S"
record.stationName.contains("MOEVE", ignoreCase = true) -> Color(0xFF555555) to "M"
record.stationName.contains("BALLENOIL", ignoreCase = true) -> Color(0xFF003399) to "B"
record.stationName.contains("PLENERGY", ignoreCase = true) -> Color(0xFF2E7D32) to "P"
else -> MaterialTheme.colorScheme.surfaceVariant to (record.stationName.firstOrNull()?.toString()?.uppercase() ?: "?")
}
Surface(
color = bColor,
modifier = Modifier.size(28.dp),
shape = CircleShape
) {
if (isRepsol) {
Image(
painter = painterResource(id = R.drawable.repsol_logo),
contentDescription = "Repsol",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
Box(contentAlignment = Alignment.Center) {
Text(bInitial, style = MaterialTheme.typography.labelSmall, color = Color.White, fontWeight = FontWeight.Bold)
}
}
}
Spacer(modifier = Modifier.width(8.dp))
Text(record.stationName, style = MaterialTheme.typography.titleMedium)
}
Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall)
}
Row {
@@ -474,9 +783,29 @@ fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit,
}
}
Text(android.text.format.DateFormat.format("dd/MM/yyyy", record.date).toString(), style = MaterialTheme.typography.labelSmall)
Spacer(modifier = Modifier.height(4.dp))
Text("${record.fuelType}: ${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium)
Text("Total: ${record.totalPrice}", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column {
Text("${record.fuelType}: ${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium)
Text("Total: ${record.totalPrice}", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
}
consumption?.let {
Column(horizontalAlignment = Alignment.End) {
Text(stringResource(R.string.consumption), style = MaterialTheme.typography.labelSmall)
Text(
text = stringResource(R.string.l_per_100km, String.format(Locale.getDefault(), "%.1f", it)),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.tertiary,
fontWeight = FontWeight.Bold
)
}
}
}
record.odometer?.let {
Text("KM: ${String.format(Locale.getDefault(), "%.0f", it)}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary)
}
}
}
}
@@ -484,6 +813,74 @@ fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit,
}
}
@Composable
fun HistoryFilterDialog(
records: List<FuelRecord>,
currentMonth: Int?,
currentYear: Int?,
onDismiss: () -> Unit,
onConfirm: (Int?, Int?) -> Unit
) {
var selectedMonth by remember { mutableStateOf(currentMonth) }
var selectedYear by remember { mutableStateOf(currentYear) }
val years = records.map {
java.util.Calendar.getInstance().apply { timeInMillis = it.date }.get(java.util.Calendar.YEAR)
}.distinct().sortedDescending()
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.filter_history)) },
text = {
Column {
Text(stringResource(R.string.month), style = MaterialTheme.typography.labelMedium)
val months = (0..11)
val context = LocalContext.current
SecondaryScrollableTabRow(
selectedTabIndex = if (selectedMonth == null) 0 else selectedMonth!! + 1,
edgePadding = 0.dp,
divider = {},
containerColor = Color.Transparent
) {
Tab(selected = selectedMonth == null, onClick = { selectedMonth = null }) {
Text(stringResource(R.string.all_months), modifier = Modifier.padding(8.dp))
}
months.forEach { m ->
val mResId = context.resources.getIdentifier("month_$m", "string", context.packageName)
Tab(selected = selectedMonth == m, onClick = { selectedMonth = m }) {
Text(if (mResId != 0) stringResource(mResId) else "", modifier = Modifier.padding(8.dp))
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(stringResource(R.string.year), style = MaterialTheme.typography.labelMedium)
SecondaryScrollableTabRow(
selectedTabIndex = if (selectedYear == null) 0 else years.indexOf(selectedYear!!) + 1,
edgePadding = 0.dp,
divider = {},
containerColor = Color.Transparent
) {
Tab(selected = selectedYear == null, onClick = { selectedYear = null }) {
Text(stringResource(R.string.all_years), modifier = Modifier.padding(8.dp))
}
years.forEach { y ->
Tab(selected = selectedYear == y, onClick = { selectedYear = y }) {
Text(y.toString(), modifier = Modifier.padding(8.dp))
}
}
}
}
},
confirmButton = {
Button(onClick = { onConfirm(selectedMonth, selectedYear) }) { Text("Filtrar") }
},
dismissButton = {
TextButton(onClick = { onConfirm(null, null) }) { Text("Limpiar") }
}
)
}
@Composable
fun FuelLogDialog(
stationName: String,
@@ -499,12 +896,17 @@ fun FuelLogDialog(
initialLiters: String = "",
initialTotal: String = "",
initialType: String = "Diésel A",
initialDate: Long = System.currentTimeMillis(),
initialOdometer: String = "",
onDismiss: () -> Unit,
onConfirm: (Double, Double, String, Double) -> Unit
onConfirm: (Double, Double, String, Double, Long, Double?) -> Unit
) {
val context = LocalContext.current
var liters by remember { mutableStateOf(initialLiters) }
var total by remember { mutableStateOf(initialTotal) }
var odometer by remember { mutableStateOf(initialOdometer) }
var selectedDate by remember { mutableStateOf(initialDate) }
var showDatePicker by remember { mutableStateOf(false) }
val dieselLabel = stringResource(R.string.diesel_a_label)
val premiumLabel = stringResource(R.string.premium_label)
@@ -538,6 +940,24 @@ fun FuelLogDialog(
}
}
if (showDatePicker) {
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = selectedDate)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
datePickerState.selectedDateMillis?.let { selectedDate = it }
showDatePicker = false
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) { Text(stringResource(R.string.cancel)) }
}
) {
DatePicker(state = datePickerState)
}
}
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.log_fuel)) },
@@ -546,6 +966,18 @@ fun FuelLogDialog(
Text(stationName, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
// Date Selection
Text(stringResource(R.string.date_label), style = MaterialTheme.typography.labelMedium)
OutlinedButton(
onClick = { showDatePicker = true },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.DateRange, null)
Spacer(modifier = Modifier.width(8.dp))
Text(android.text.format.DateFormat.format("dd/MM/yyyy", selectedDate).toString())
}
Spacer(modifier = Modifier.height(8.dp))
Text(stringResource(R.string.fuel_type), style = MaterialTheme.typography.labelMedium)
FlowRow(
modifier = Modifier.fillMaxWidth(),
@@ -646,13 +1078,20 @@ fun FuelLogDialog(
label = { Text(stringResource(R.string.total_price)) },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = odometer,
onValueChange = { odometer = it },
label = { Text(stringResource(R.string.odometer)) },
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
Button(onClick = {
val l = liters.toDoubleOrNull() ?: 0.0
val t = total.toDoubleOrNull() ?: 0.0
if (l > 0 && t > 0) onConfirm(l, t, selectedType, if (l > 0) t / l else currentPrice)
val o = odometer.toDoubleOrNull()
if (l > 0 && t > 0) onConfirm(l, t, selectedType, if (l > 0) t / l else currentPrice, selectedDate, o)
}) { Text(stringResource(R.string.save)) }
},
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }
@@ -660,20 +1099,31 @@ fun FuelLogDialog(
}
@Composable
fun GasStationItem(station: GasStationInfo, selectedFuelType: FuelType, onClick: () -> Unit, onLogClick: () -> Unit) {
fun GasStationItem(
station: GasStationInfo,
selectedFuelType: FuelType,
isFavorite: Boolean,
onClick: () -> Unit,
onLogClick: () -> Unit,
onToggleFavorite: () -> Unit
) {
val priceColor = when (station.priceCategory) {
PriceCategory.CHEAP -> Color(0xFF2E7D32)
PriceCategory.NORMAL -> MaterialTheme.colorScheme.primary
PriceCategory.EXPENSIVE -> Color(0xFFC62828)
}
val brandColor = when {
station.name.contains("REPSOL", ignoreCase = true) -> Color(0xFFE30613)
station.name.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000)
station.name.contains("BP", ignoreCase = true) -> Color(0xFF00A94F)
station.name.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00)
station.name.contains("SHELL", ignoreCase = true) -> Color(0xFFFFD500)
else -> MaterialTheme.colorScheme.surfaceVariant
val isRepsol = station.name.contains("REPSOL", ignoreCase = true)
val (brandColor, brandInitial) = when {
isRepsol -> Color(0xFFE30613) to "R"
station.name.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000) to "C"
station.name.contains("BP", ignoreCase = true) -> Color(0xFF00A94F) to "B"
station.name.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00) to "G"
station.name.contains("SHELL", ignoreCase = true) -> Color(0xFFFFD500) to "S"
station.name.contains("MOEVE", ignoreCase = true) -> Color(0xFF555555) to "M"
station.name.contains("BALLENOIL", ignoreCase = true) -> Color(0xFF003399) to "B"
station.name.contains("PLENERGY", ignoreCase = true) -> Color(0xFF2E7D32) to "P"
else -> MaterialTheme.colorScheme.surfaceVariant to (station.name.firstOrNull()?.toString()?.uppercase() ?: "?")
}
Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp).clickable { onClick() }, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)) {
@@ -681,8 +1131,39 @@ fun GasStationItem(station: GasStationInfo, selectedFuelType: FuelType, onClick:
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(color = brandColor, modifier = Modifier.size(12.dp).padding(end = 8.dp), shape = androidx.compose.foundation.shape.CircleShape) {}
Surface(
color = brandColor,
modifier = Modifier.size(32.dp),
shape = CircleShape
) {
if (isRepsol) {
Image(
painter = painterResource(id = R.drawable.repsol_logo),
contentDescription = "Repsol",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
Box(contentAlignment = Alignment.Center) {
Text(
text = brandInitial,
style = MaterialTheme.typography.titleSmall,
color = if (brandColor == MaterialTheme.colorScheme.surfaceVariant) MaterialTheme.colorScheme.onSurface else Color.White,
fontWeight = FontWeight.Bold
)
}
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(text = "${station.name} - ${station.municipality}", style = MaterialTheme.typography.titleMedium, maxLines = 1, modifier = Modifier.weight(1f))
IconButton(onClick = onToggleFavorite, modifier = Modifier.size(24.dp)) {
Icon(
imageVector = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = null,
tint = if (isFavorite) Color.Red else LocalContentColor.current,
modifier = Modifier.size(18.dp)
)
}
}
Text(text = station.address, style = MaterialTheme.typography.bodySmall, maxLines = 1)
if (station.horario.isNotEmpty()) {
@@ -18,7 +18,7 @@ class GasStationRepository {
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "GasolinerasApp/1.0 (Android; Contact: support@example.com)")
.header("User-Agent", "Mozilla/5.0 (Android) GasolinerasApp/1.0")
.build()
chain.proceed(request)
}
@@ -138,8 +138,8 @@ class GasStationRepository {
} else null
} ?: emptyList()
// Radio máximo: 30km local / 400km viaje
val finalMaxDist = if (destinationLocation == null) 30.0 else 400.0
// Radio máximo: 30km local / 1000km viaje
val finalMaxDist = if (destinationLocation == null) 30.0 else 1000.0
val filtered = allStations.filter { (it.distance / 1000.0) <= finalMaxDist }
if (filtered.isEmpty()) return@withContext emptyList<GasStationInfo>() to emptyList<Location>()
@@ -10,6 +10,7 @@ import com.example.prueba.R
import com.example.prueba.data.FuelType
import com.example.prueba.data.GasStationInfo
import com.example.prueba.data.GasStationRepository
import com.example.prueba.utils.FavoriteStorage
import com.example.prueba.utils.FuelRecord
import com.example.prueba.utils.FuelStorage
import com.example.prueba.utils.LocationStorage
@@ -34,7 +35,12 @@ data class GasStationUiState(
val destinationQuery: String = "",
val isRouteActive: Boolean = false,
val destinationLocation: Location? = null,
val routePoints: List<Location> = emptyList()
val routePoints: List<Location> = emptyList(),
val mapCenterLocation: Location? = null,
val favoriteIds: Set<String> = emptySet(),
val showOnlyFavorites: Boolean = false,
val filterMonth: Int? = null, // null for all
val filterYear: Int? = null // null for all
)
class GasStationViewModel(
@@ -56,6 +62,28 @@ class GasStationViewModel(
fun selectTab(index: Int) {
_uiState.update { it.copy(selectedTab = index, errorMessage = null, visibleStationsCount = 15) }
if (index == 0 || index == 2) {
loadFavorites()
}
}
fun toggleFavorite(stationId: String) {
FavoriteStorage.toggleFavorite(context, stationId)
loadFavorites()
}
fun toggleFavoritesFilter() {
_uiState.update { it.copy(showOnlyFavorites = !it.showOnlyFavorites) }
refreshStations()
}
fun setHistoryFilters(month: Int?, year: Int?) {
_uiState.update { it.copy(filterMonth = month, filterYear = year) }
}
private fun loadFavorites() {
val favs = FavoriteStorage.getFavorites(context)
_uiState.update { it.copy(favoriteIds = favs) }
}
fun selectFuelType(type: FuelType) {
@@ -177,18 +205,39 @@ class GasStationViewModel(
_uiState.update { it.copy(visibleStationsCount = 15) }
}
fun updateMapCenter(lat: Double, lon: Double) {
val loc = Location("").apply {
latitude = lat
longitude = lon
}
_uiState.update { it.copy(mapCenterLocation = loc) }
}
fun searchAtMapCenter() {
val center = _uiState.value.mapCenterLocation ?: return
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
val (stations, _) = repository.getCheapestAndNearest(
userLocation = center,
fuelType = _uiState.value.selectedFuelType
)
_uiState.update { it.copy(stations = stations, isLoading = false) }
}
}
fun refreshStations(forceRefresh: Boolean = false) {
val state = _uiState.value
val loc = state.lastLocation ?: return
if (state.selectedTab != 0) return
// Refrescar solo en pestañas de datos
if (state.selectedTab != 0 && state.selectedTab != 2) return
viewModelScope.launch {
try {
if (forceRefresh) _uiState.update { it.copy(isRefreshing = true, errorMessage = null) }
else if (state.stations.isEmpty()) _uiState.update { it.copy(isLoading = true, errorMessage = null) }
// 1. Fast load
// 1. Carga Rápida (Línea recta)
val (fastStations, points) = repository.getCheapestAndNearest(
userLocation = loc,
getRealDistances = false,
@@ -197,23 +246,27 @@ class GasStationViewModel(
destinationLocation = if (state.isRouteActive) state.destinationLocation else null
)
if (fastStations.isEmpty() && forceRefresh) {
_uiState.update { it.copy(errorMessage = context.getString(R.string.no_results)) }
}
// Aplicar el filtro de favoritos ANTES de actualizar la UI para evitar parpadeos
val finalFast = if (_uiState.value.showOnlyFavorites) {
fastStations.filter { _uiState.value.favoriteIds.contains(it.id) }
} else fastStations
_uiState.update { it.copy(stations = fastStations, routePoints = points, isLoading = false) }
_uiState.update { it.copy(stations = finalFast, routePoints = points, isLoading = false) }
// 2. Precision load
// 2. Carga de Precisión (Carretera)
val (realStations, _) = repository.getCheapestAndNearest(
userLocation = loc,
getRealDistances = true,
forceRefresh = false, // don't force twice
forceRefresh = false,
fuelType = state.selectedFuelType,
destinationLocation = if (state.isRouteActive) state.destinationLocation else null
)
if (realStations.isNotEmpty()) {
_uiState.update { it.copy(stations = realStations, isRefreshing = false) }
val finalReal = if (_uiState.value.showOnlyFavorites) {
realStations.filter { _uiState.value.favoriteIds.contains(it.id) }
} else realStations
_uiState.update { it.copy(stations = finalReal, isRefreshing = false) }
} else {
_uiState.update { it.copy(isRefreshing = false) }
}
@@ -230,15 +283,20 @@ class GasStationViewModel(
}
init {
loadFavorites()
viewModelScope.launch {
combine(
_uiState.map { it.lastLocation }.distinctUntilChanged(),
_uiState.map { it.selectedTab }.distinctUntilChanged(),
_uiState.map { it.selectedFuelType }.distinctUntilChanged(),
_uiState.map { it.isRouteActive }.distinctUntilChanged(),
_uiState.map { it.destinationLocation }.distinctUntilChanged()
) { lastLoc, tab, _, _, _ ->
if (lastLoc != null && tab == 0) {
_uiState.map { it.destinationLocation }.distinctUntilChanged(),
_uiState.map { it.showOnlyFavorites }.distinctUntilChanged()
) { args ->
val lastLoc = args[0] as? Location
val tab = args[1] as Int
// Ahora refresca tanto en pestaña 0 (Lista) como en pestaña 2 (Mapa)
if (lastLoc != null && (tab == 0 || tab == 2)) {
refreshStations()
}
}.collect()
@@ -0,0 +1,30 @@
package com.example.prueba.utils
import android.content.Context
object FavoriteStorage {
private const val PREFS_NAME = "favorite_prefs"
private const val KEY_FAVORITES = "favorite_ids"
fun toggleFavorite(context: Context, stationId: String) {
val favorites = getFavorites(context).toMutableSet()
if (favorites.contains(stationId)) {
favorites.remove(stationId)
} else {
favorites.add(stationId)
}
saveFavorites(context, favorites)
}
fun getFavorites(context: Context): Set<String> {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getStringSet(KEY_FAVORITES, emptySet()) ?: emptySet()
}
private fun saveFavorites(context: Context, favorites: Set<String>) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putStringSet(KEY_FAVORITES, favorites)
.apply()
}
}
@@ -12,7 +12,8 @@ data class FuelRecord(
val liters: Double,
val totalPrice: Double,
val pricePerLiter: Double,
val fuelType: String = "Diésel A"
val fuelType: String = "Diésel A",
val odometer: Double? = null
)
object FuelStorage {
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+30
View File
@@ -36,4 +36,34 @@
<string name="gnl_label">GNL</string>
<string name="diesel_b_label">Diésel B</string>
<string name="biodiesel_label">Biodiesel</string>
<string name="search_this_area">Buscar en esta zona</string>
<string name="go_to_waze">Ir en Waze</string>
<string name="favorites">Favoritos</string>
<string name="add_favorite">Añadir a favoritos</string>
<string name="remove_favorite">Quitar de favoritos</string>
<string name="show_favorites">Mostrar solo favoritos</string>
<string name="date_label">Fecha</string>
<string name="select_date">Seleccionar fecha</string>
<string name="monthly_spend">Gasto este mes</string>
<string name="yearly_spend">Gasto este año</string>
<string name="filter_history">Filtrar historial</string>
<string name="all_months">Todos los meses</string>
<string name="all_years">Todos los años</string>
<string name="month">Mes</string>
<string name="year">Año</string>
<string name="month_0">Enero</string>
<string name="month_1">Febrero</string>
<string name="month_2">Marzo</string>
<string name="month_3">Abril</string>
<string name="month_4">Mayo</string>
<string name="month_5">Junio</string>
<string name="month_6">Julio</string>
<string name="month_7">Agosto</string>
<string name="month_8">Septiembre</string>
<string name="month_9">Octubre</string>
<string name="month_10">Noviembre</string>
<string name="month_11">Diciembre</string>
<string name="odometer">Kilómetros totales (Opcional)</string>
<string name="consumption">Consumo</string>
<string name="l_per_100km">%1$s L/100km</string>
</resources>