Gasolinera app
This commit is contained in:
+50
@@ -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>
|
||||||
@@ -3,15 +3,13 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.type.automotive"
|
|
||||||
android:required="false" />
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.software.car.templates_host"
|
android:name="android.software.car.templates_host"
|
||||||
android:required="false" />
|
android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -23,12 +21,12 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Prueba">
|
android:theme="@style/Theme.Prueba">
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.gms.car.application"
|
|
||||||
android:resource="@xml/automotive_app_desc" />
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="androidx.car.app.minCarApiLevel"
|
android:name="androidx.car.app.minCarApiLevel"
|
||||||
android:value="1" />
|
android:value="1" />
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.car.app.category"
|
||||||
|
android:value="poi" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -38,15 +36,19 @@
|
|||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Indicar al sistema que la actividad está optimizada para el coche -->
|
||||||
|
<meta-data
|
||||||
|
android:name="distractionOptimized"
|
||||||
|
android:value="true" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".MyCarAppService"
|
android:name=".MyCarAppService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:permission="androidx.car.app.PERMISSION_CAR_APP">
|
android:permission="androidx.car.app.PERMISSION_CAR_APP">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="androidx.car.app.CarAppService" />
|
<action android:name="androidx.car.app.CarAppService" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.prueba
|
package com.example.prueba
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -10,10 +11,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.AltRoute
|
import androidx.compose.material.icons.automirrored.filled.AltRoute
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
@@ -23,7 +26,9 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.GasStationViewModel
|
||||||
import com.example.prueba.ui.theme.PruebaTheme
|
import com.example.prueba.ui.theme.PruebaTheme
|
||||||
import com.example.prueba.utils.FuelRecord
|
import com.example.prueba.utils.FuelRecord
|
||||||
import android.content.Context
|
|
||||||
import org.osmdroid.config.Configuration
|
import org.osmdroid.config.Configuration
|
||||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.osmdroid.views.MapView
|
import org.osmdroid.views.MapView
|
||||||
import org.osmdroid.views.overlay.Marker
|
import org.osmdroid.views.overlay.Marker
|
||||||
import org.osmdroid.views.overlay.Polyline
|
import org.osmdroid.views.overlay.Polyline
|
||||||
import android.preference.PreferenceManager
|
import java.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -52,8 +56,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// OSMDroid configuration
|
// OSMDroid configuration
|
||||||
Configuration.getInstance().load(this, getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
|
val osmConfig = Configuration.getInstance()
|
||||||
Configuration.getInstance().userAgentValue = "GasolinerasApp/1.0 (Android; Contact: support@example.com)"
|
osmConfig.userAgentValue = "Mozilla/5.0 (Android) GasolinerasApp/1.0"
|
||||||
|
osmConfig.load(this, getSharedPreferences("osmdroid_prefs", MODE_PRIVATE))
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
@@ -73,6 +78,8 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
|
|||||||
|
|
||||||
var showFuelDialog by remember { mutableStateOf<GasStationInfo?>(null) }
|
var showFuelDialog by remember { mutableStateOf<GasStationInfo?>(null) }
|
||||||
var editRecordDialog by remember { mutableStateOf<FuelRecord?>(null) }
|
var editRecordDialog by remember { mutableStateOf<FuelRecord?>(null) }
|
||||||
|
var selectedStationForMap by remember { mutableStateOf<GasStationInfo?>(null) }
|
||||||
|
var showHistoryFilter by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
@@ -148,14 +155,36 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
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) }) {
|
IconButton(onClick = { viewModel.setRouteActive(true) }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.AltRoute, stringResource(R.string.on_route))
|
Icon(Icons.AutoMirrored.Filled.AltRoute, stringResource(R.string.on_route))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (uiState.selectedTab == 0) {
|
if (uiState.selectedTab == 0 || uiState.selectedTab == 2) {
|
||||||
FuelTypeSelector(
|
FuelTypeSelector(
|
||||||
selectedType = uiState.selectedFuelType,
|
selectedType = uiState.selectedFuelType,
|
||||||
onTypeSelected = { viewModel.selectFuelType(it) }
|
onTypeSelected = { viewModel.selectFuelType(it) }
|
||||||
@@ -209,9 +238,11 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
|
|||||||
stations = uiState.stations,
|
stations = uiState.stations,
|
||||||
visibleCount = uiState.visibleStationsCount,
|
visibleCount = uiState.visibleStationsCount,
|
||||||
selectedFuelType = uiState.selectedFuelType,
|
selectedFuelType = uiState.selectedFuelType,
|
||||||
|
favoriteIds = uiState.favoriteIds,
|
||||||
onLogClick = { showFuelDialog = it },
|
onLogClick = { showFuelDialog = it },
|
||||||
onShowMore = { viewModel.showMoreStations() },
|
onShowMore = { viewModel.showMoreStations() },
|
||||||
onShowLess = { viewModel.resetStationLimit() }
|
onShowLess = { viewModel.resetStationLimit() },
|
||||||
|
onToggleFavorite = { viewModel.toggleFavorite(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,17 +250,99 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
|
|||||||
1 -> {
|
1 -> {
|
||||||
FuelHistoryScreen(
|
FuelHistoryScreen(
|
||||||
records = uiState.fuelRecords,
|
records = uiState.fuelRecords,
|
||||||
|
filterMonth = uiState.filterMonth,
|
||||||
|
filterYear = uiState.filterYear,
|
||||||
onDelete = { record -> viewModel.deleteFuelRecord(record) },
|
onDelete = { record -> viewModel.deleteFuelRecord(record) },
|
||||||
onEdit = { record -> editRecordDialog = record }
|
onEdit = { record -> editRecordDialog = record }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
GasStationMap(
|
GasStationMap(
|
||||||
stations = uiState.stations,
|
stations = uiState.stations,
|
||||||
routePoints = uiState.routePoints,
|
routePoints = uiState.routePoints,
|
||||||
userLocation = uiState.lastLocation,
|
userLocation = uiState.lastLocation,
|
||||||
selectedFuelType = uiState.selectedFuelType
|
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)
|
FuelType.BIODIESEL -> stringResource(R.string.biodiesel_label)
|
||||||
},
|
},
|
||||||
onDismiss = { showFuelDialog = null },
|
onDismiss = { showFuelDialog = null },
|
||||||
onConfirm = { liters, total, type, pricePerLiter ->
|
onConfirm = { liters, total, type, pricePerLiter, date, odo ->
|
||||||
val record = FuelRecord(
|
val record = FuelRecord(
|
||||||
date = System.currentTimeMillis(),
|
date = date,
|
||||||
stationName = station.name,
|
stationName = station.name,
|
||||||
address = station.address,
|
address = station.address,
|
||||||
municipality = station.municipality,
|
municipality = station.municipality,
|
||||||
liters = liters,
|
liters = liters,
|
||||||
totalPrice = total,
|
totalPrice = total,
|
||||||
pricePerLiter = pricePerLiter,
|
pricePerLiter = pricePerLiter,
|
||||||
fuelType = type
|
fuelType = type,
|
||||||
|
odometer = odo
|
||||||
)
|
)
|
||||||
viewModel.saveFuelRecord(record)
|
viewModel.saveFuelRecord(record)
|
||||||
showFuelDialog = null
|
showFuelDialog = null
|
||||||
@@ -275,19 +389,24 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editRecordDialog != null) {
|
||||||
editRecordDialog?.let { record ->
|
editRecordDialog?.let { record ->
|
||||||
FuelLogDialog(
|
FuelLogDialog(
|
||||||
stationName = record.stationName,
|
stationName = record.stationName,
|
||||||
initialLiters = record.liters.toString(),
|
initialLiters = record.liters.toString(),
|
||||||
initialTotal = record.totalPrice.toString(),
|
initialTotal = record.totalPrice.toString(),
|
||||||
initialType = record.fuelType,
|
initialType = record.fuelType,
|
||||||
|
initialDate = record.date,
|
||||||
|
initialOdometer = record.odometer?.toString() ?: "",
|
||||||
onDismiss = { editRecordDialog = null },
|
onDismiss = { editRecordDialog = null },
|
||||||
onConfirm = { liters, total, type, pricePerLiter ->
|
onConfirm = { liters, total, type, pricePerLiter, date, odo ->
|
||||||
val newRecord = record.copy(
|
val newRecord = record.copy(
|
||||||
|
date = date,
|
||||||
liters = liters,
|
liters = liters,
|
||||||
totalPrice = total,
|
totalPrice = total,
|
||||||
fuelType = type,
|
fuelType = type,
|
||||||
pricePerLiter = pricePerLiter
|
pricePerLiter = pricePerLiter,
|
||||||
|
odometer = odo
|
||||||
)
|
)
|
||||||
viewModel.updateFuelRecord(record, newRecord)
|
viewModel.updateFuelRecord(record, newRecord)
|
||||||
editRecordDialog = null
|
editRecordDialog = null
|
||||||
@@ -295,6 +414,20 @@ fun GasStationApp(viewModel: GasStationViewModel = viewModel()) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>,
|
stations: List<GasStationInfo>,
|
||||||
routePoints: List<android.location.Location>,
|
routePoints: List<android.location.Location>,
|
||||||
userLocation: android.location.Location?,
|
userLocation: android.location.Location?,
|
||||||
selectedFuelType: FuelType
|
selectedFuelType: FuelType,
|
||||||
|
favoriteIds: Set<String>,
|
||||||
|
onMapMoved: (Double, Double) -> Unit,
|
||||||
|
onStationSelected: (GasStationInfo) -> Unit
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
MapView(context).apply {
|
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)
|
setMultiTouchControls(true)
|
||||||
|
setLayerType(android.view.View.LAYER_TYPE_SOFTWARE, null)
|
||||||
|
|
||||||
controller.setZoom(15.0)
|
controller.setZoom(15.0)
|
||||||
userLocation?.let {
|
userLocation?.let {
|
||||||
controller.setCenter(GeoPoint(it.latitude, it.longitude))
|
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 ->
|
update = { mapView ->
|
||||||
mapView.overlays.clear()
|
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()) {
|
if (routePoints.isNotEmpty()) {
|
||||||
val line = Polyline(mapView)
|
val line = Polyline(mapView)
|
||||||
line.setPoints(routePoints.map { GeoPoint(it.latitude, it.longitude) })
|
line.setPoints(routePoints.map { GeoPoint(it.latitude, it.longitude) })
|
||||||
@@ -327,24 +489,30 @@ fun GasStationMap(
|
|||||||
line.outlinePaint.strokeWidth = 10f
|
line.outlinePaint.strokeWidth = 10f
|
||||||
mapView.overlays.add(line)
|
mapView.overlays.add(line)
|
||||||
|
|
||||||
// Zoom para ajustar a la ruta la primera vez
|
// Zoom para ajustar a la ruta si es nueva
|
||||||
if (mapView.tag == null) {
|
if (mapView.tag != "route_zoomed") {
|
||||||
val points = routePoints.map { GeoPoint(it.latitude, it.longitude) }
|
val points = routePoints.map { GeoPoint(it.latitude, it.longitude) }
|
||||||
|
if (points.isNotEmpty()) {
|
||||||
mapView.zoomToBoundingBox(org.osmdroid.util.BoundingBox.fromGeoPoints(points), true)
|
mapView.zoomToBoundingBox(org.osmdroid.util.BoundingBox.fromGeoPoints(points), true)
|
||||||
mapView.tag = "zoomed"
|
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 {
|
userLocation?.let {
|
||||||
val userMarker = Marker(mapView)
|
val userMarker = Marker(mapView)
|
||||||
userMarker.position = GeoPoint(it.latitude, it.longitude)
|
userMarker.position = GeoPoint(it.latitude, it.longitude)
|
||||||
userMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
userMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||||
userMarker.title = "Tu ubicación"
|
userMarker.title = "Tu ubicación"
|
||||||
|
userMarker.icon = ContextCompat.getDrawable(mapView.context, org.osmdroid.library.R.drawable.person)
|
||||||
mapView.overlays.add(userMarker)
|
mapView.overlays.add(userMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Marcadores de gasolineras
|
// 4. Marcadores de gasolineras con colores
|
||||||
stations.forEach { station ->
|
stations.forEach { station ->
|
||||||
val marker = Marker(mapView)
|
val marker = Marker(mapView)
|
||||||
marker.position = GeoPoint(station.latitude, station.longitude)
|
marker.position = GeoPoint(station.latitude, station.longitude)
|
||||||
@@ -354,6 +522,27 @@ fun GasStationMap(
|
|||||||
marker.title = "${station.name}: $price €/L"
|
marker.title = "${station.name}: $price €/L"
|
||||||
marker.subDescription = station.address
|
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)
|
mapView.overlays.add(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,9 +594,11 @@ fun GasStationList(
|
|||||||
stations: List<GasStationInfo>,
|
stations: List<GasStationInfo>,
|
||||||
visibleCount: Int,
|
visibleCount: Int,
|
||||||
selectedFuelType: FuelType,
|
selectedFuelType: FuelType,
|
||||||
|
favoriteIds: Set<String>,
|
||||||
onLogClick: (GasStationInfo) -> Unit,
|
onLogClick: (GasStationInfo) -> Unit,
|
||||||
onShowMore: () -> Unit,
|
onShowMore: () -> Unit,
|
||||||
onShowLess: () -> Unit
|
onShowLess: () -> Unit,
|
||||||
|
onToggleFavorite: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val visibleStations = stations.take(visibleCount)
|
val visibleStations = stations.take(visibleCount)
|
||||||
@@ -417,11 +608,13 @@ fun GasStationList(
|
|||||||
GasStationItem(
|
GasStationItem(
|
||||||
station = station,
|
station = station,
|
||||||
selectedFuelType = selectedFuelType,
|
selectedFuelType = selectedFuelType,
|
||||||
|
isFavorite = favoriteIds.contains(station.id),
|
||||||
onClick = {
|
onClick = {
|
||||||
val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}(${Uri.encode(station.name)})")
|
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))
|
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
},
|
},
|
||||||
onLogClick = { onLogClick(station) }
|
onLogClick = { onLogClick(station) },
|
||||||
|
onToggleFavorite = { onToggleFavorite(station.id) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,17 +647,133 @@ fun GasStationList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit, onEdit: (FuelRecord) -> Unit) {
|
fun FuelHistoryScreen(
|
||||||
if (records.isEmpty()) {
|
records: List<FuelRecord>,
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(R.string.no_records)) }
|
filterMonth: Int?,
|
||||||
} else {
|
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)) {
|
LazyColumn(contentPadding = PaddingValues(16.dp)) {
|
||||||
items(records) { record ->
|
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)) {
|
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
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.stationName, style = MaterialTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall)
|
Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
@@ -474,15 +783,103 @@ fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(android.text.format.DateFormat.format("dd/MM/yyyy", record.date).toString(), style = MaterialTheme.typography.labelSmall)
|
Text(android.text.format.DateFormat.format("dd/MM/yyyy", record.date).toString(), style = MaterialTheme.typography.labelSmall)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Column {
|
||||||
Text("${record.fuelType}: ${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium)
|
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)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
@Composable
|
||||||
fun FuelLogDialog(
|
fun FuelLogDialog(
|
||||||
@@ -499,12 +896,17 @@ fun FuelLogDialog(
|
|||||||
initialLiters: String = "",
|
initialLiters: String = "",
|
||||||
initialTotal: String = "",
|
initialTotal: String = "",
|
||||||
initialType: String = "Diésel A",
|
initialType: String = "Diésel A",
|
||||||
|
initialDate: Long = System.currentTimeMillis(),
|
||||||
|
initialOdometer: String = "",
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: (Double, Double, String, Double) -> Unit
|
onConfirm: (Double, Double, String, Double, Long, Double?) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var liters by remember { mutableStateOf(initialLiters) }
|
var liters by remember { mutableStateOf(initialLiters) }
|
||||||
var total by remember { mutableStateOf(initialTotal) }
|
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 dieselLabel = stringResource(R.string.diesel_a_label)
|
||||||
val premiumLabel = stringResource(R.string.premium_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(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.log_fuel)) },
|
title = { Text(stringResource(R.string.log_fuel)) },
|
||||||
@@ -546,6 +966,18 @@ fun FuelLogDialog(
|
|||||||
Text(stationName, style = MaterialTheme.typography.bodyLarge)
|
Text(stationName, style = MaterialTheme.typography.bodyLarge)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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)
|
Text(stringResource(R.string.fuel_type), style = MaterialTheme.typography.labelMedium)
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -646,13 +1078,20 @@ fun FuelLogDialog(
|
|||||||
label = { Text(stringResource(R.string.total_price)) },
|
label = { Text(stringResource(R.string.total_price)) },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = odometer,
|
||||||
|
onValueChange = { odometer = it },
|
||||||
|
label = { Text(stringResource(R.string.odometer)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
val l = liters.toDoubleOrNull() ?: 0.0
|
val l = liters.toDoubleOrNull() ?: 0.0
|
||||||
val t = total.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)) }
|
}) { Text(stringResource(R.string.save)) }
|
||||||
},
|
},
|
||||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }
|
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }
|
||||||
@@ -660,20 +1099,31 @@ fun FuelLogDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) {
|
val priceColor = when (station.priceCategory) {
|
||||||
PriceCategory.CHEAP -> Color(0xFF2E7D32)
|
PriceCategory.CHEAP -> Color(0xFF2E7D32)
|
||||||
PriceCategory.NORMAL -> MaterialTheme.colorScheme.primary
|
PriceCategory.NORMAL -> MaterialTheme.colorScheme.primary
|
||||||
PriceCategory.EXPENSIVE -> Color(0xFFC62828)
|
PriceCategory.EXPENSIVE -> Color(0xFFC62828)
|
||||||
}
|
}
|
||||||
|
|
||||||
val brandColor = when {
|
val isRepsol = station.name.contains("REPSOL", ignoreCase = true)
|
||||||
station.name.contains("REPSOL", ignoreCase = true) -> Color(0xFFE30613)
|
val (brandColor, brandInitial) = when {
|
||||||
station.name.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000)
|
isRepsol -> Color(0xFFE30613) to "R"
|
||||||
station.name.contains("BP", ignoreCase = true) -> Color(0xFF00A94F)
|
station.name.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000) to "C"
|
||||||
station.name.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00)
|
station.name.contains("BP", ignoreCase = true) -> Color(0xFF00A94F) to "B"
|
||||||
station.name.contains("SHELL", ignoreCase = true) -> Color(0xFFFFD500)
|
station.name.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00) to "G"
|
||||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
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)) {
|
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) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
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))
|
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)
|
Text(text = station.address, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||||
if (station.horario.isNotEmpty()) {
|
if (station.horario.isNotEmpty()) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class GasStationRepository {
|
|||||||
private val client = OkHttpClient.Builder()
|
private val client = OkHttpClient.Builder()
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
val request = chain.request().newBuilder()
|
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()
|
.build()
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
}
|
}
|
||||||
@@ -138,8 +138,8 @@ class GasStationRepository {
|
|||||||
} else null
|
} else null
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
|
||||||
// Radio máximo: 30km local / 400km viaje
|
// Radio máximo: 30km local / 1000km viaje
|
||||||
val finalMaxDist = if (destinationLocation == null) 30.0 else 400.0
|
val finalMaxDist = if (destinationLocation == null) 30.0 else 1000.0
|
||||||
val filtered = allStations.filter { (it.distance / 1000.0) <= finalMaxDist }
|
val filtered = allStations.filter { (it.distance / 1000.0) <= finalMaxDist }
|
||||||
|
|
||||||
if (filtered.isEmpty()) return@withContext emptyList<GasStationInfo>() to emptyList<Location>()
|
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.FuelType
|
||||||
import com.example.prueba.data.GasStationInfo
|
import com.example.prueba.data.GasStationInfo
|
||||||
import com.example.prueba.data.GasStationRepository
|
import com.example.prueba.data.GasStationRepository
|
||||||
|
import com.example.prueba.utils.FavoriteStorage
|
||||||
import com.example.prueba.utils.FuelRecord
|
import com.example.prueba.utils.FuelRecord
|
||||||
import com.example.prueba.utils.FuelStorage
|
import com.example.prueba.utils.FuelStorage
|
||||||
import com.example.prueba.utils.LocationStorage
|
import com.example.prueba.utils.LocationStorage
|
||||||
@@ -34,7 +35,12 @@ data class GasStationUiState(
|
|||||||
val destinationQuery: String = "",
|
val destinationQuery: String = "",
|
||||||
val isRouteActive: Boolean = false,
|
val isRouteActive: Boolean = false,
|
||||||
val destinationLocation: Location? = null,
|
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(
|
class GasStationViewModel(
|
||||||
@@ -56,6 +62,28 @@ class GasStationViewModel(
|
|||||||
|
|
||||||
fun selectTab(index: Int) {
|
fun selectTab(index: Int) {
|
||||||
_uiState.update { it.copy(selectedTab = index, errorMessage = null, visibleStationsCount = 15) }
|
_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) {
|
fun selectFuelType(type: FuelType) {
|
||||||
@@ -177,18 +205,39 @@ class GasStationViewModel(
|
|||||||
_uiState.update { it.copy(visibleStationsCount = 15) }
|
_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) {
|
fun refreshStations(forceRefresh: Boolean = false) {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
val loc = state.lastLocation ?: return
|
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 {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
if (forceRefresh) _uiState.update { it.copy(isRefreshing = true, errorMessage = null) }
|
if (forceRefresh) _uiState.update { it.copy(isRefreshing = true, errorMessage = null) }
|
||||||
else if (state.stations.isEmpty()) _uiState.update { it.copy(isLoading = 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(
|
val (fastStations, points) = repository.getCheapestAndNearest(
|
||||||
userLocation = loc,
|
userLocation = loc,
|
||||||
getRealDistances = false,
|
getRealDistances = false,
|
||||||
@@ -197,23 +246,27 @@ class GasStationViewModel(
|
|||||||
destinationLocation = if (state.isRouteActive) state.destinationLocation else null
|
destinationLocation = if (state.isRouteActive) state.destinationLocation else null
|
||||||
)
|
)
|
||||||
|
|
||||||
if (fastStations.isEmpty() && forceRefresh) {
|
// Aplicar el filtro de favoritos ANTES de actualizar la UI para evitar parpadeos
|
||||||
_uiState.update { it.copy(errorMessage = context.getString(R.string.no_results)) }
|
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(
|
val (realStations, _) = repository.getCheapestAndNearest(
|
||||||
userLocation = loc,
|
userLocation = loc,
|
||||||
getRealDistances = true,
|
getRealDistances = true,
|
||||||
forceRefresh = false, // don't force twice
|
forceRefresh = false,
|
||||||
fuelType = state.selectedFuelType,
|
fuelType = state.selectedFuelType,
|
||||||
destinationLocation = if (state.isRouteActive) state.destinationLocation else null
|
destinationLocation = if (state.isRouteActive) state.destinationLocation else null
|
||||||
)
|
)
|
||||||
|
|
||||||
if (realStations.isNotEmpty()) {
|
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 {
|
} else {
|
||||||
_uiState.update { it.copy(isRefreshing = false) }
|
_uiState.update { it.copy(isRefreshing = false) }
|
||||||
}
|
}
|
||||||
@@ -230,15 +283,20 @@ class GasStationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
loadFavorites()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
combine(
|
||||||
_uiState.map { it.lastLocation }.distinctUntilChanged(),
|
_uiState.map { it.lastLocation }.distinctUntilChanged(),
|
||||||
_uiState.map { it.selectedTab }.distinctUntilChanged(),
|
_uiState.map { it.selectedTab }.distinctUntilChanged(),
|
||||||
_uiState.map { it.selectedFuelType }.distinctUntilChanged(),
|
_uiState.map { it.selectedFuelType }.distinctUntilChanged(),
|
||||||
_uiState.map { it.isRouteActive }.distinctUntilChanged(),
|
_uiState.map { it.isRouteActive }.distinctUntilChanged(),
|
||||||
_uiState.map { it.destinationLocation }.distinctUntilChanged()
|
_uiState.map { it.destinationLocation }.distinctUntilChanged(),
|
||||||
) { lastLoc, tab, _, _, _ ->
|
_uiState.map { it.showOnlyFavorites }.distinctUntilChanged()
|
||||||
if (lastLoc != null && tab == 0) {
|
) { 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()
|
refreshStations()
|
||||||
}
|
}
|
||||||
}.collect()
|
}.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 liters: Double,
|
||||||
val totalPrice: Double,
|
val totalPrice: Double,
|
||||||
val pricePerLiter: Double,
|
val pricePerLiter: Double,
|
||||||
val fuelType: String = "Diésel A"
|
val fuelType: String = "Diésel A",
|
||||||
|
val odometer: Double? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
object FuelStorage {
|
object FuelStorage {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -36,4 +36,34 @@
|
|||||||
<string name="gnl_label">GNL</string>
|
<string name="gnl_label">GNL</string>
|
||||||
<string name="diesel_b_label">Diésel B</string>
|
<string name="diesel_b_label">Diésel B</string>
|
||||||
<string name="biodiesel_label">Biodiesel</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user