Gasolinera app
This commit is contained in:
Generated
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+1
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="jbr-25" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="jbr-25" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -12,12 +12,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.animation.AnimatedVisibility
|
|
||||||
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.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.AltRoute
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
@@ -28,12 +28,12 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
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.data.PriceCategory
|
import com.example.prueba.data.PriceCategory
|
||||||
import com.example.prueba.ui.theme.PruebaTheme
|
import com.example.prueba.ui.theme.PruebaTheme
|
||||||
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
|
||||||
@@ -71,8 +71,11 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var destinationQuery by remember { mutableStateOf("") }
|
||||||
|
var isRouteActive by remember { mutableStateOf(false) }
|
||||||
|
var isReserveFilterActive by remember { mutableStateOf(false) }
|
||||||
|
var destinationLocation by remember { mutableStateOf<android.location.Location?>(null) }
|
||||||
|
|
||||||
var hasPermission by remember {
|
var hasPermission by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
@@ -98,33 +101,36 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
val address = addresses[0]
|
val address = addresses[0]
|
||||||
val street = address.thoroughfare
|
val street = address.thoroughfare
|
||||||
val number = address.subThoroughfare
|
val number = address.subThoroughfare
|
||||||
|
currentAddress = if (street != null && number != null) "$street, $number" else street ?: address.getAddressLine(0) ?: "Ubicación desconocida"
|
||||||
currentAddress = if (street != null && number != null) {
|
|
||||||
"$street, $number"
|
|
||||||
} else {
|
|
||||||
street ?: address.getAddressLine(0) ?: "Ubicación desconocida"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (ignore: Exception) {}
|
} catch (ignore: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(hasPermission, searchQuery, selectedTab) {
|
LaunchedEffect(hasPermission, destinationLocation, selectedTab, isReserveFilterActive) {
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
|
||||||
val loc = lastLocation ?: LocationStorage.getLastLocation(context)
|
val loc = lastLocation ?: LocationStorage.getLastLocation(context)
|
||||||
|
|
||||||
loc?.let {
|
loc?.let {
|
||||||
|
// Primero recta para rapidez
|
||||||
stations = repository.getCheapestAndNearest(
|
stations = repository.getCheapestAndNearest(
|
||||||
context = context,
|
|
||||||
userLocation = it,
|
userLocation = it,
|
||||||
municipalityQuery = if (selectedTab == 0) searchQuery.ifBlank { null } else null,
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
onlyFavorites = (selectedTab == 1)
|
destinationLocation = if (isRouteActive) destinationLocation else null,
|
||||||
|
getRealDistances = false
|
||||||
|
)
|
||||||
|
// Luego real para precisión
|
||||||
|
scope.launch {
|
||||||
|
stations = repository.getCheapestAndNearest(
|
||||||
|
userLocation = it,
|
||||||
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
|
destinationLocation = if (isRouteActive) destinationLocation else null,
|
||||||
|
getRealDistances = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (selectedTab == 0) isLoading = true
|
||||||
|
|
||||||
if (selectedTab != 2) isLoading = true
|
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 15000)
|
||||||
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000)
|
|
||||||
.setMinUpdateDistanceMeters(500f)
|
.setMinUpdateDistanceMeters(500f)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -133,10 +139,15 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
val location = result.lastLocation ?: return
|
val location = result.lastLocation ?: return
|
||||||
lastLocation = location
|
lastLocation = location
|
||||||
LocationStorage.saveLocation(context, location)
|
LocationStorage.saveLocation(context, location)
|
||||||
if (searchQuery.isBlank()) {
|
if (selectedTab == 0) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
updateAddress(location)
|
updateAddress(location)
|
||||||
stations = repository.getCheapestAndNearest(context, location)
|
stations = repository.getCheapestAndNearest(
|
||||||
|
userLocation = location,
|
||||||
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
|
destinationLocation = if (isRouteActive) destinationLocation else null,
|
||||||
|
getRealDistances = true
|
||||||
|
)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -145,6 +156,7 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||||
try {
|
try {
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, android.os.Looper.getMainLooper())
|
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, android.os.Looper.getMainLooper())
|
||||||
@@ -161,42 +173,50 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
Column {
|
Column {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
if (isSearchActive) {
|
if (isRouteActive) {
|
||||||
TextField(
|
TextField(
|
||||||
value = searchQuery,
|
value = destinationQuery,
|
||||||
onValueChange = { searchQuery = it },
|
onValueChange = { destinationQuery = it },
|
||||||
placeholder = { Text("Buscar municipio...") },
|
placeholder = { Text("¿A dónde vas?") },
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedContainerColor = Color.Transparent
|
|
||||||
),
|
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
Row {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
searchQuery = ""
|
scope.launch {
|
||||||
isSearchActive = false
|
withContext(Dispatchers.IO) {
|
||||||
}) {
|
try {
|
||||||
Icon(Icons.Default.Close, contentDescription = "Cerrar")
|
val geocoder = Geocoder(context, Locale.getDefault())
|
||||||
|
val results = geocoder.getFromLocationName(destinationQuery, 1)
|
||||||
|
if (!results.isNullOrEmpty()) {
|
||||||
|
destinationLocation = android.location.Location("").apply {
|
||||||
|
latitude = results[0].latitude
|
||||||
|
longitude = results[0].longitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) { Icon(Icons.Default.DirectionsCar, null) }
|
||||||
|
IconButton(onClick = {
|
||||||
|
isRouteActive = false
|
||||||
|
destinationLocation = null
|
||||||
|
destinationQuery = ""
|
||||||
|
}) { Icon(Icons.Default.Close, null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text("Gasolineras Diésel", style = MaterialTheme.typography.titleSmall)
|
Text("Gasolineras Diésel", style = MaterialTheme.typography.titleSmall)
|
||||||
Text(
|
Text(text = currentAddress, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, maxLines = 1)
|
||||||
text = currentAddress,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isSearchActive && selectedTab != 2) {
|
if (!isRouteActive && selectedTab == 0) {
|
||||||
IconButton(onClick = { isSearchActive = true }) {
|
IconButton(onClick = { isRouteActive = true }) {
|
||||||
Icon(Icons.Default.Search, contentDescription = "Buscar")
|
Icon(Icons.AutoMirrored.Filled.AltRoute, "En ruta")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,99 +226,63 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(Icons.Default.Place, "Gasolineras") },
|
icon = { Icon(Icons.Default.LocalGasStation, null) },
|
||||||
label = { Text("Diésel") },
|
label = { Text("Gasolineras") },
|
||||||
selected = selectedTab == 0,
|
selected = selectedTab == 0,
|
||||||
onClick = { selectedTab = 0 }
|
onClick = { selectedTab = 0 }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(Icons.Default.Favorite, "Favoritas") },
|
icon = { Icon(Icons.Default.History, null) },
|
||||||
label = { Text("Favoritas") },
|
label = { Text("Historial") },
|
||||||
selected = selectedTab == 1,
|
selected = selectedTab == 1,
|
||||||
onClick = { selectedTab = 1 }
|
onClick = { selectedTab = 1 }
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
|
||||||
icon = { Icon(Icons.Default.DateRange, "Historial") },
|
|
||||||
label = { Text("Historial") },
|
|
||||||
selected = selectedTab == 2,
|
|
||||||
onClick = { selectedTab = 2 }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
|
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
Column(
|
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text("Se necesita permiso de ubicación")
|
Text("Se necesita permiso de ubicación")
|
||||||
Button(onClick = { launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }) {
|
Button(onClick = { launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }) { Text("Dar permiso") }
|
||||||
Text("Dar permiso")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
0, 1 -> {
|
0 -> {
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
lastLocation?.let { loc ->
|
lastLocation?.let { loc ->
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null)
|
stations = repository.getCheapestAndNearest(
|
||||||
|
userLocation = loc, forceRefresh = true,
|
||||||
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
|
destinationLocation = if (isRouteActive) destinationLocation else null
|
||||||
|
)
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (selectedTab == 0) {
|
|
||||||
if (isLoading && stations.isEmpty()) {
|
if (isLoading && stations.isEmpty()) {
|
||||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
} else if (stations.isEmpty()) {
|
} else if (stations.isEmpty()) {
|
||||||
Text("No se encontraron gasolineras", modifier = Modifier.align(Alignment.Center))
|
Text(if (isReserveFilterActive) "No hay nada a menos de 40km" else "Sin resultados", modifier = Modifier.align(Alignment.Center))
|
||||||
} else {
|
} else {
|
||||||
GasStationList(
|
GasStationList(stations = stations, onLogClick = { showFuelDialog = it })
|
||||||
stations = stations,
|
|
||||||
context = context,
|
|
||||||
scope = scope,
|
|
||||||
repository = repository,
|
|
||||||
lastLocation = lastLocation,
|
|
||||||
onLogClick = { showFuelDialog = it },
|
|
||||||
onUpdateStations = { stations = it },
|
|
||||||
searchQuery = searchQuery
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (stations.isEmpty()) {
|
|
||||||
Text("No tienes gasolineras favoritas marcadas", modifier = Modifier.align(Alignment.Center))
|
|
||||||
} else {
|
|
||||||
GasStationList(
|
|
||||||
stations = stations,
|
|
||||||
context = context,
|
|
||||||
scope = scope,
|
|
||||||
repository = repository,
|
|
||||||
lastLocation = lastLocation,
|
|
||||||
onLogClick = { showFuelDialog = it },
|
|
||||||
onUpdateStations = { stations = it },
|
|
||||||
searchQuery = ""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
1 -> {
|
||||||
2 -> {
|
|
||||||
FuelHistoryScreen(
|
FuelHistoryScreen(
|
||||||
records = fuelRecords,
|
records = fuelRecords,
|
||||||
onDelete = { record ->
|
onDelete = { record ->
|
||||||
FuelStorage.deleteRecord(context, record)
|
FuelStorage.deleteRecord(context, record)
|
||||||
fuelRecords = FuelStorage.getRecords(context)
|
fuelRecords = FuelStorage.getRecords(context)
|
||||||
},
|
},
|
||||||
onEdit = { record ->
|
onEdit = { record -> editRecordDialog = record }
|
||||||
editRecordDialog = record
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,10 +316,7 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
initialTotal = record.totalPrice.toString(),
|
initialTotal = record.totalPrice.toString(),
|
||||||
onDismiss = { editRecordDialog = null },
|
onDismiss = { editRecordDialog = null },
|
||||||
onConfirm = { liters, total ->
|
onConfirm = { liters, total ->
|
||||||
val newRecord = record.copy(
|
val newRecord = record.copy(liters = liters, totalPrice = total)
|
||||||
liters = liters,
|
|
||||||
totalPrice = total
|
|
||||||
)
|
|
||||||
FuelStorage.updateRecord(context, record, newRecord)
|
FuelStorage.updateRecord(context, record, newRecord)
|
||||||
fuelRecords = FuelStorage.getRecords(context)
|
fuelRecords = FuelStorage.getRecords(context)
|
||||||
editRecordDialog = null
|
editRecordDialog = null
|
||||||
@@ -347,35 +328,17 @@ fun GasStationApp(repository: GasStationRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GasStationList(
|
fun GasStationList(stations: List<GasStationInfo>, onLogClick: (GasStationInfo) -> Unit) {
|
||||||
stations: List<GasStationInfo>,
|
val context = LocalContext.current
|
||||||
context: android.content.Context,
|
|
||||||
scope: kotlinx.coroutines.CoroutineScope,
|
|
||||||
repository: GasStationRepository,
|
|
||||||
lastLocation: android.location.Location?,
|
|
||||||
onLogClick: (GasStationInfo) -> Unit,
|
|
||||||
onUpdateStations: (List<GasStationInfo>) -> Unit,
|
|
||||||
searchQuery: String
|
|
||||||
) {
|
|
||||||
LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) {
|
LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) {
|
||||||
items(stations) { station ->
|
items(stations) { station ->
|
||||||
GasStationItem(
|
GasStationItem(
|
||||||
station = station,
|
station = station,
|
||||||
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)})")
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
context.startActivity(intent)
|
|
||||||
},
|
},
|
||||||
onLogClick = { onLogClick(station) },
|
onLogClick = { onLogClick(station) }
|
||||||
onFavoriteClick = {
|
|
||||||
FavoriteStorage.toggleFavorite(context, station.id)
|
|
||||||
val loc = lastLocation ?: LocationStorage.getLastLocation(context)
|
|
||||||
loc?.let {
|
|
||||||
scope.launch {
|
|
||||||
onUpdateStations(repository.getCheapestAndNearest(context, it, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,16 +347,11 @@ fun GasStationList(
|
|||||||
@Composable
|
@Composable
|
||||||
fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit, onEdit: (FuelRecord) -> Unit) {
|
fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit, onEdit: (FuelRecord) -> Unit) {
|
||||||
if (records.isEmpty()) {
|
if (records.isEmpty()) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Sin registros") }
|
||||||
Text("Aún no tienes repostajes registrados")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(contentPadding = PaddingValues(16.dp)) {
|
LazyColumn(contentPadding = PaddingValues(16.dp)) {
|
||||||
items(records) { record ->
|
items(records) { record ->
|
||||||
Card(
|
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
|
||||||
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)) {
|
||||||
@@ -401,26 +359,15 @@ fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit,
|
|||||||
Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall)
|
Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
IconButton(onClick = { onEdit(record) }, modifier = Modifier.size(24.dp)) {
|
IconButton(onClick = { onEdit(record) }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.Edit, null, modifier = Modifier.size(18.dp)) }
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Editar", modifier = Modifier.size(18.dp))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
IconButton(onClick = { onDelete(record) }, modifier = Modifier.size(24.dp)) {
|
IconButton(onClick = { onDelete(record) }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.Delete, null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.error) }
|
||||||
Icon(Icons.Default.Delete, contentDescription = "Eliminar", modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
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))
|
||||||
Text("${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium)
|
Text("${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium)
|
||||||
Text(
|
Text("Total: ${record.totalPrice} €", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
|
||||||
"Total: ${record.totalPrice} €",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,13 +376,7 @@ fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FuelLogDialog(
|
fun FuelLogDialog(stationName: String, initialLiters: String = "", initialTotal: String = "", onDismiss: () -> Unit, onConfirm: (Double, Double) -> Unit) {
|
||||||
stationName: String,
|
|
||||||
initialLiters: String = "",
|
|
||||||
initialTotal: String = "",
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: (Double, Double) -> Unit
|
|
||||||
) {
|
|
||||||
var liters by remember { mutableStateOf(initialLiters) }
|
var liters by remember { mutableStateOf(initialLiters) }
|
||||||
var total by remember { mutableStateOf(initialTotal) }
|
var total by remember { mutableStateOf(initialTotal) }
|
||||||
|
|
||||||
@@ -446,18 +387,8 @@ fun FuelLogDialog(
|
|||||||
Column {
|
Column {
|
||||||
Text(stationName, style = MaterialTheme.typography.bodyLarge)
|
Text(stationName, style = MaterialTheme.typography.bodyLarge)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
OutlinedTextField(
|
OutlinedTextField(value = liters, onValueChange = { liters = it }, label = { Text("Litros") }, modifier = Modifier.fillMaxWidth())
|
||||||
value = liters,
|
OutlinedTextField(value = total, onValueChange = { total = it }, label = { Text("Precio Total (€)") }, modifier = Modifier.fillMaxWidth())
|
||||||
onValueChange = { liters = it },
|
|
||||||
label = { Text("Litros") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = total,
|
|
||||||
onValueChange = { total = it },
|
|
||||||
label = { Text("Precio Total (€)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
@@ -467,14 +398,12 @@ fun FuelLogDialog(
|
|||||||
if (l > 0 && t > 0) onConfirm(l, t)
|
if (l > 0 && t > 0) onConfirm(l, t)
|
||||||
}) { Text("Guardar") }
|
}) { Text("Guardar") }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = { TextButton(onClick = onDismiss) { Text("Cancelar") } }
|
||||||
TextButton(onClick = onDismiss) { Text("Cancelar") }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () -> Unit, onFavoriteClick: () -> Unit) {
|
fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () -> 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
|
||||||
@@ -490,86 +419,43 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: ()
|
|||||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp).clickable { onClick() }, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 6.dp)
|
|
||||||
.clickable { onClick() },
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Row(
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
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(
|
Surface(color = brandColor, modifier = Modifier.size(12.dp).padding(end = 8.dp), shape = androidx.compose.foundation.shape.CircleShape) {}
|
||||||
color = brandColor,
|
Text(text = "${station.name} - ${station.municipality}", style = MaterialTheme.typography.titleMedium, maxLines = 1, modifier = Modifier.weight(1f))
|
||||||
modifier = Modifier.size(12.dp).padding(end = 8.dp),
|
|
||||||
shape = androidx.compose.foundation.shape.CircleShape
|
|
||||||
) {}
|
|
||||||
Text(
|
|
||||||
text = "${station.name} - ${station.municipality}",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
IconButton(onClick = onFavoriteClick) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (station.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
|
||||||
contentDescription = "Favorito",
|
|
||||||
tint = if (station.isFavorite) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
Text(text = station.address, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||||
}
|
}
|
||||||
|
Surface(color = MaterialTheme.colorScheme.secondaryContainer, shape = MaterialTheme.shapes.medium) {
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(
|
Text(
|
||||||
text = station.address,
|
text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km (recta)",
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
|
station.durationMinutes?.let { mins ->
|
||||||
|
Text(
|
||||||
|
text = "$mins min",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(text = "Diésel A: ${station.price} €/L", style = MaterialTheme.typography.titleLarge, color = priceColor, fontWeight = FontWeight.Bold)
|
||||||
text = "Diésel A: ${station.price} €/L",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = priceColor,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
station.pricePremium?.let { premium ->
|
station.pricePremium?.let { premium ->
|
||||||
Text(
|
Text(text = "Premium: $premium €/L", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary, fontWeight = FontWeight.Medium)
|
||||||
text = "Premium: $premium €/L",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(
|
Button(onClick = onLogClick, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), shape = MaterialTheme.shapes.small) {
|
||||||
onClick = onLogClick,
|
Icon(Icons.Default.Add, null, modifier = Modifier.size(16.dp))
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("Registrar")
|
Text("Registrar")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ import androidx.car.app.model.*
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
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
|
||||||
import com.google.android.gms.location.*
|
import com.google.android.gms.location.*
|
||||||
import com.google.android.gms.tasks.CancellationTokenSource
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -26,6 +24,7 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
private var gasStations: List<GasStationInfo> = emptyList()
|
private var gasStations: List<GasStationInfo> = emptyList()
|
||||||
private var currentAddress: String? = null
|
private var currentAddress: String? = null
|
||||||
private var isLoading = true
|
private var isLoading = true
|
||||||
|
private var isReserveFilterActive = false
|
||||||
private val repository = GasStationRepository()
|
private val repository = GasStationRepository()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -41,16 +40,9 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
val address = addresses[0]
|
val address = addresses[0]
|
||||||
val street = address.thoroughfare
|
val street = address.thoroughfare
|
||||||
val number = address.subThoroughfare
|
val number = address.subThoroughfare
|
||||||
|
currentAddress = if (street != null && number != null) "$street, $number" else street ?: address.getAddressLine(0)
|
||||||
currentAddress = if (street != null && number != null) {
|
|
||||||
"$street, $number"
|
|
||||||
} else {
|
|
||||||
street ?: address.getAddressLine(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Silencioso
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +50,23 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
private fun fetchLocationAndGasStations(forceRefresh: Boolean = false) {
|
private fun fetchLocationAndGasStations(forceRefresh: Boolean = false) {
|
||||||
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(carContext)
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(carContext)
|
||||||
|
|
||||||
// Cargar última ubicación guardada inmediatamente si no es un refresh forzado
|
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
LocationStorage.getLastLocation(carContext)?.let { lastLoc ->
|
LocationStorage.getLastLocation(carContext)?.let { lastLoc ->
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
updateAddress(lastLoc)
|
updateAddress(lastLoc)
|
||||||
gasStations = repository.getCheapestAndNearest(carContext, lastLoc)
|
// Primero recta
|
||||||
|
gasStations = repository.getCheapestAndNearest(
|
||||||
|
userLocation = lastLoc,
|
||||||
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
|
getRealDistances = false
|
||||||
|
)
|
||||||
|
invalidate()
|
||||||
|
// Luego real
|
||||||
|
gasStations = repository.getCheapestAndNearest(
|
||||||
|
userLocation = lastLoc,
|
||||||
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
|
getRealDistances = true
|
||||||
|
)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,79 +82,60 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
LocationStorage.saveLocation(carContext, location)
|
LocationStorage.saveLocation(carContext, location)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
updateAddress(location)
|
updateAddress(location)
|
||||||
gasStations = repository.getCheapestAndNearest(carContext, location, forceRefresh)
|
gasStations = repository.getCheapestAndNearest(
|
||||||
|
userLocation = location,
|
||||||
|
forceRefresh = forceRefresh,
|
||||||
|
maxDistanceKm = if (isReserveFilterActive) 40.0 else null,
|
||||||
|
getRealDistances = true
|
||||||
|
)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
// Si era un forceRefresh, después de la primera actualización volvemos a false
|
|
||||||
// para que los updates automáticos del GPS no sigan forzando red innecesariamente
|
|
||||||
if (forceRefresh) {
|
|
||||||
fusedLocationClient.removeLocationUpdates(this)
|
|
||||||
fetchLocationAndGasStations(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fusedLocationClient.requestLocationUpdates(
|
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, carContext.mainLooper)
|
||||||
locationRequest,
|
|
||||||
locationCallback,
|
|
||||||
carContext.mainLooper
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
val listBuilder = ItemList.Builder()
|
val listBuilder = ItemList.Builder()
|
||||||
|
|
||||||
val refreshAction = Action.Builder()
|
val reserveAction = Action.Builder()
|
||||||
.setTitle("Refrescar")
|
.setTitle(if (isReserveFilterActive) "Todo" else "Reserva")
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
|
isReserveFilterActive = !isReserveFilterActive
|
||||||
isLoading = true
|
isLoading = true
|
||||||
invalidate()
|
invalidate()
|
||||||
fetchLocationAndGasStations(forceRefresh = true)
|
fetchLocationAndGasStations(forceRefresh = false)
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (isLoading && gasStations.isEmpty()) {
|
if (isLoading && gasStations.isEmpty()) {
|
||||||
return MessageTemplate.Builder("Buscando gasolineras...")
|
return MessageTemplate.Builder("Buscando...")
|
||||||
.setLoading(true)
|
.setLoading(true)
|
||||||
.setHeader(
|
.setHeader(Header.Builder().setTitle("Gasolineras").build())
|
||||||
Header.Builder()
|
|
||||||
.setStartHeaderAction(Action.APP_ICON)
|
|
||||||
.setTitle("Gasolineras")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gasStations.isEmpty()) {
|
if (gasStations.isEmpty()) {
|
||||||
return MessageTemplate.Builder("No se encontraron gasolineras o no hay permiso de ubicación.")
|
return MessageTemplate.Builder("Sin resultados")
|
||||||
.setHeader(
|
.setHeader(Header.Builder().setTitle("Gasolineras").addEndHeaderAction(reserveAction).build())
|
||||||
Header.Builder()
|
.addAction(Action.Builder().setTitle("Reintentar").setOnClickListener {
|
||||||
.setStartHeaderAction(Action.APP_ICON)
|
|
||||||
.setTitle("Gasolineras")
|
|
||||||
.addEndHeaderAction(refreshAction)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setTitle("Reintentar")
|
|
||||||
.setOnClickListener {
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
invalidate()
|
invalidate()
|
||||||
fetchLocationAndGasStations()
|
fetchLocationAndGasStations()
|
||||||
}
|
}.build())
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (station in gasStations) {
|
for (station in gasStations) {
|
||||||
val distanceStr = String.format(Locale.getDefault(), "%.1f km", station.distance / 1000)
|
val distanceStr = String.format(Locale.getDefault(), "%.1f km", station.distance / 1000)
|
||||||
|
val durationStr = station.durationMinutes?.let { " ($it min)" } ?: ""
|
||||||
|
|
||||||
val rowBuilder = Row.Builder()
|
val rowBuilder = Row.Builder()
|
||||||
.setTitle("${station.name} - ${station.municipality}")
|
.setTitle("${station.name} - ${station.municipality}")
|
||||||
.addText("A: ${station.price} €/L" + (station.pricePremium?.let { " • P: $it €/L" } ?: ""))
|
.addText("A: ${station.price} €/L" + (station.pricePremium?.let { " • P: $it €/L" } ?: ""))
|
||||||
.addText("$distanceStr (recta) • ${station.address}")
|
.addText("$distanceStr$durationStr • ${station.address}")
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
val paneBuilder = Pane.Builder()
|
val paneBuilder = Pane.Builder()
|
||||||
.addRow(Row.Builder().setTitle("Diésel A").addText("${station.price} €/L").build())
|
.addRow(Row.Builder().setTitle("Diésel A").addText("${station.price} €/L").build())
|
||||||
@@ -162,31 +146,12 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
|
|
||||||
val pane = paneBuilder
|
val pane = paneBuilder
|
||||||
.addRow(Row.Builder().setTitle("Ubicación").addText("${station.municipality}\n${station.address}").build())
|
.addRow(Row.Builder().setTitle("Ubicación").addText("${station.municipality}\n${station.address}").build())
|
||||||
.addRow(Row.Builder().setTitle("Distancia").addText("$distanceStr (en línea recta)").build())
|
.addRow(Row.Builder().setTitle("Distancia").addText("$distanceStr$durationStr").build())
|
||||||
.addAction(
|
.addAction(Action.Builder().setTitle("Ir ahora").setOnClickListener {
|
||||||
Action.Builder()
|
|
||||||
.setTitle(if (station.isFavorite) "Quitar Favorito" else "Añadir Favorito")
|
|
||||||
.setOnClickListener {
|
|
||||||
FavoriteStorage.toggleFavorite(carContext, station.id)
|
|
||||||
screenManager.pop()
|
|
||||||
fetchLocationAndGasStations(forceRefresh = false)
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setTitle("Ir ahora")
|
|
||||||
.setOnClickListener {
|
|
||||||
val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}")
|
val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}")
|
||||||
val intent = Intent(CarContext.ACTION_NAVIGATE, uri)
|
carContext.startCarApp(Intent(CarContext.ACTION_NAVIGATE, uri))
|
||||||
carContext.startCarApp(intent)
|
}.build())
|
||||||
}
|
.addAction(Action.Builder().setTitle("Registrar").setOnClickListener {
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setTitle("Registrar")
|
|
||||||
.setOnClickListener {
|
|
||||||
val record = FuelRecord(
|
val record = FuelRecord(
|
||||||
date = System.currentTimeMillis(),
|
date = System.currentTimeMillis(),
|
||||||
stationName = station.name,
|
stationName = station.name,
|
||||||
@@ -198,22 +163,15 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
)
|
)
|
||||||
FuelStorage.saveRecord(carContext, record)
|
FuelStorage.saveRecord(carContext, record)
|
||||||
screenManager.pop()
|
screenManager.pop()
|
||||||
}
|
}.build())
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
screenManager.push(
|
screenManager.push(object : Screen(carContext) {
|
||||||
object : Screen(carContext) {
|
override fun onGetTemplate(): Template = PaneTemplate.Builder(pane)
|
||||||
override fun onGetTemplate(): Template {
|
|
||||||
return PaneTemplate.Builder(pane)
|
|
||||||
.setHeader(Header.Builder().setTitle(station.name).setStartHeaderAction(Action.BACK).build())
|
.setHeader(Header.Builder().setTitle(station.name).setStartHeaderAction(Action.BACK).build())
|
||||||
.build()
|
.build()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
listBuilder.addItem(rowBuilder.build())
|
listBuilder.addItem(rowBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,9 +179,8 @@ class MainScreen(carContext: CarContext) : Screen(carContext) {
|
|||||||
.setSingleList(listBuilder.build())
|
.setSingleList(listBuilder.build())
|
||||||
.setHeader(
|
.setHeader(
|
||||||
Header.Builder()
|
Header.Builder()
|
||||||
.setStartHeaderAction(Action.APP_ICON)
|
.setTitle(currentAddress ?: "Gasolineras Diésel")
|
||||||
.setTitle(currentAddress ?: "Gasolineras más baratas")
|
.addEndHeaderAction(reserveAction)
|
||||||
.addEndHeaderAction(refreshAction)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.example.prueba.data
|
package com.example.prueba.data
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import com.example.prueba.utils.FavoriteStorage
|
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
|
||||||
class GasStationRepository {
|
class GasStationRepository {
|
||||||
private var cachedResponse: GasolineraResponse? = null
|
private var cachedResponse: GasolineraResponse? = null
|
||||||
@@ -15,20 +16,24 @@ class GasStationRepository {
|
|||||||
.build()
|
.build()
|
||||||
.create(GasolineraApi::class.java)
|
.create(GasolineraApi::class.java)
|
||||||
|
|
||||||
|
private val routeApi: OsrmApi = Retrofit.Builder()
|
||||||
|
.baseUrl("https://router.project-osrm.org/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(OsrmApi::class.java)
|
||||||
|
|
||||||
suspend fun getCheapestAndNearest(
|
suspend fun getCheapestAndNearest(
|
||||||
context: Context,
|
|
||||||
userLocation: Location,
|
userLocation: Location,
|
||||||
forceRefresh: Boolean = false,
|
forceRefresh: Boolean = false,
|
||||||
municipalityQuery: String? = null,
|
maxDistanceKm: Double? = null,
|
||||||
onlyFavorites: Boolean = false
|
destinationLocation: Location? = null,
|
||||||
|
getRealDistances: Boolean = false
|
||||||
): List<GasStationInfo> {
|
): List<GasStationInfo> {
|
||||||
return try {
|
return try {
|
||||||
if (cachedResponse == null || forceRefresh) {
|
if (cachedResponse == null || forceRefresh) {
|
||||||
cachedResponse = api.getGasolineras()
|
cachedResponse = api.getGasolineras()
|
||||||
}
|
}
|
||||||
|
|
||||||
val favorites = FavoriteStorage.getFavoriteIds(context)
|
|
||||||
|
|
||||||
val allStations = cachedResponse?.listaEESS?.mapNotNull {
|
val allStations = cachedResponse?.listaEESS?.mapNotNull {
|
||||||
val lat = it.latitud.replace(",", ".").toDoubleOrNull()
|
val lat = it.latitud.replace(",", ".").toDoubleOrNull()
|
||||||
val lon = it.longitud.replace(",", ".").toDoubleOrNull()
|
val lon = it.longitud.replace(",", ".").toDoubleOrNull()
|
||||||
@@ -42,7 +47,17 @@ class GasStationRepository {
|
|||||||
latitude = lat
|
latitude = lat
|
||||||
longitude = lon
|
longitude = lon
|
||||||
}
|
}
|
||||||
val distance = userLocation.distanceTo(stationLoc)
|
val distanceToUser = userLocation.distanceTo(stationLoc)
|
||||||
|
val validPrice = (dieselPrice ?: 0.0) > 0.1
|
||||||
|
|
||||||
|
var isOnRoute = true
|
||||||
|
if (destinationLocation != null) {
|
||||||
|
val distUserToDest = userLocation.distanceTo(destinationLocation)
|
||||||
|
val distStationToDest = stationLoc.distanceTo(destinationLocation)
|
||||||
|
isOnRoute = (distanceToUser + distStationToDest) <= (distUserToDest * 1.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnRoute && validPrice) {
|
||||||
GasStationInfo(
|
GasStationInfo(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
name = it.rotulo,
|
name = it.rotulo,
|
||||||
@@ -50,37 +65,59 @@ class GasStationRepository {
|
|||||||
municipality = it.municipio,
|
municipality = it.municipio,
|
||||||
price = dieselPrice ?: 0.0,
|
price = dieselPrice ?: 0.0,
|
||||||
pricePremium = premiumPrice,
|
pricePremium = premiumPrice,
|
||||||
distance = distance,
|
distance = distanceToUser,
|
||||||
latitude = lat,
|
latitude = lat,
|
||||||
longitude = lon,
|
longitude = lon
|
||||||
isFavorite = favorites.contains(it.id)
|
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
|
} else null
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
|
||||||
val filteredStations = when {
|
// Radio máximo: 30km si es local, 400km si hay destino (ruta larga)
|
||||||
onlyFavorites -> allStations.filter { it.isFavorite }
|
val finalMaxDist = maxDistanceKm ?: if (destinationLocation == null) 30.0 else 400.0
|
||||||
.sortedBy { it.distance }
|
|
||||||
!municipalityQuery.isNullOrBlank() -> allStations.filter { it.municipality.contains(municipalityQuery, ignoreCase = true) }
|
|
||||||
.sortedBy { it.distance }
|
|
||||||
else -> allStations.sortedBy { it.distance }.take(50)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filteredStations.isEmpty()) return emptyList()
|
val filtered = allStations.filter { (it.distance / 1000.0) <= finalMaxDist }
|
||||||
|
|
||||||
val minPrice = filteredStations.minOf { it.price }
|
if (filtered.isEmpty()) return emptyList()
|
||||||
val avgPrice = filteredStations.map { it.price }.average()
|
|
||||||
|
|
||||||
filteredStations.map { station ->
|
val minPrice = filtered.minOf { it.price }
|
||||||
|
val avgPrice = filtered.map { it.price }.average()
|
||||||
|
|
||||||
|
var results = filtered.map { station ->
|
||||||
val category = when {
|
val category = when {
|
||||||
station.price <= minPrice * 1.01 -> PriceCategory.CHEAP
|
station.price <= minPrice * 1.01 -> PriceCategory.CHEAP
|
||||||
station.price <= avgPrice -> PriceCategory.NORMAL
|
station.price <= avgPrice -> PriceCategory.NORMAL
|
||||||
else -> PriceCategory.EXPENSIVE
|
else -> PriceCategory.EXPENSIVE
|
||||||
}
|
}
|
||||||
station.copy(priceCategory = category)
|
station.copy(priceCategory = category)
|
||||||
}.sortedWith(compareByDescending<GasStationInfo> { it.isFavorite }.thenBy { it.price })
|
}.sortedBy { it.distance }
|
||||||
.take(if (onlyFavorites) 50 else 15)
|
.take(if (destinationLocation != null) 30 else 15)
|
||||||
|
|
||||||
|
if (getRealDistances) {
|
||||||
|
coroutineScope {
|
||||||
|
val tasks = results.map { station ->
|
||||||
|
async {
|
||||||
|
try {
|
||||||
|
val coords = "${userLocation.longitude},${userLocation.latitude};${station.longitude},${station.latitude}"
|
||||||
|
val routeResponse = routeApi.getRoute(coords)
|
||||||
|
val route = routeResponse.routes.firstOrNull()
|
||||||
|
if (route != null) {
|
||||||
|
station.copy(
|
||||||
|
distance = route.distance.toFloat(),
|
||||||
|
durationMinutes = (route.duration / 60.0).toInt()
|
||||||
|
)
|
||||||
|
} else station
|
||||||
|
} catch (e: Exception) {
|
||||||
|
station
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = tasks.awaitAll().sortedBy { it.distance }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
@@ -97,8 +134,8 @@ data class GasStationInfo(
|
|||||||
val price: Double,
|
val price: Double,
|
||||||
val pricePremium: Double? = null,
|
val pricePremium: Double? = null,
|
||||||
val distance: Float,
|
val distance: Float,
|
||||||
|
val durationMinutes: Int? = null,
|
||||||
val latitude: Double,
|
val latitude: Double,
|
||||||
val longitude: Double,
|
val longitude: Double,
|
||||||
val isFavorite: Boolean = false,
|
|
||||||
val priceCategory: PriceCategory = PriceCategory.NORMAL
|
val priceCategory: PriceCategory = PriceCategory.NORMAL
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.example.prueba.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
data class OsrmResponse(
|
||||||
|
@SerializedName("routes")
|
||||||
|
val routes: List<OsrmRoute>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OsrmRoute(
|
||||||
|
@SerializedName("distance")
|
||||||
|
val distance: Double, // En metros
|
||||||
|
@SerializedName("duration")
|
||||||
|
val duration: Double // En segundos
|
||||||
|
)
|
||||||
|
|
||||||
|
interface OsrmApi {
|
||||||
|
@GET("route/v1/driving/{coords}?overview=false")
|
||||||
|
suspend fun getRoute(
|
||||||
|
@Path("coords") coords: String
|
||||||
|
): OsrmResponse
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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 prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
val favorites = getFavoriteIds(context).toMutableSet()
|
|
||||||
|
|
||||||
if (favorites.contains(stationId)) {
|
|
||||||
favorites.remove(stationId)
|
|
||||||
} else {
|
|
||||||
favorites.add(stationId)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit().putStringSet(KEY_FAVORITES, favorites).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFavoriteIds(context: Context): Set<String> {
|
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
return prefs.getStringSet(KEY_FAVORITES, emptySet()) ?: emptySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isFavorite(context: Context, stationId: String): Boolean {
|
|
||||||
return getFavoriteIds(context).contains(stationId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user