Gasolinera app

This commit is contained in:
2026-08-10 12:42:28 +02:00
parent 3d839131bd
commit b6857bc422
4 changed files with 249 additions and 132 deletions
@@ -12,6 +12,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -23,18 +24,20 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.example.prueba.data.GasStationInfo
import com.example.prueba.data.GasStationRepository
import com.example.prueba.data.PriceCategory
import com.example.prueba.ui.theme.PruebaTheme
import com.example.prueba.utils.FavoriteStorage
import com.example.prueba.utils.FuelRecord
import com.example.prueba.utils.FuelStorage
import com.example.prueba.utils.LocationStorage
import com.google.android.gms.location.*
import com.google.android.gms.tasks.CancellationTokenSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -68,6 +71,8 @@ fun GasStationApp(repository: GasStationRepository) {
var isLoading by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) }
var selectedTab by remember { mutableIntStateOf(0) }
var searchQuery by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) }
var hasPermission by remember {
mutableStateOf(
@@ -104,17 +109,21 @@ fun GasStationApp(repository: GasStationRepository) {
}
}
LaunchedEffect(hasPermission) {
LaunchedEffect(hasPermission, searchQuery, selectedTab) {
if (hasPermission) {
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
LocationStorage.getLastLocation(context)?.let { lastLoc ->
scope.launch {
updateAddress(lastLoc)
stations = repository.getCheapestAndNearest(context, lastLoc)
}
val loc = lastLocation ?: LocationStorage.getLastLocation(context)
loc?.let {
stations = repository.getCheapestAndNearest(
context = context,
userLocation = it,
municipalityQuery = if (selectedTab == 0) searchQuery.ifBlank { null } else null,
onlyFavorites = (selectedTab == 1)
)
}
isLoading = true
if (selectedTab != 2) isLoading = true
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000)
.setMinUpdateDistanceMeters(500f)
.build()
@@ -124,9 +133,13 @@ fun GasStationApp(repository: GasStationRepository) {
val location = result.lastLocation ?: return
lastLocation = location
LocationStorage.saveLocation(context, location)
scope.launch {
updateAddress(location)
stations = repository.getCheapestAndNearest(context, location)
if (searchQuery.isBlank()) {
scope.launch {
updateAddress(location)
stations = repository.getCheapestAndNearest(context, location)
isLoading = false
}
} else {
isLoading = false
}
}
@@ -145,25 +158,56 @@ fun GasStationApp(repository: GasStationRepository) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
CenterAlignedTopAppBar(
title = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Gasolineras Baratas", style = MaterialTheme.typography.titleSmall)
Text(
text = currentAddress,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
maxLines = 1
)
Column {
CenterAlignedTopAppBar(
title = {
if (isSearchActive) {
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text("Buscar municipio...") },
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent
),
trailingIcon = {
IconButton(onClick = {
searchQuery = ""
isSearchActive = false
}) {
Icon(Icons.Default.Close, contentDescription = "Cerrar")
}
}
)
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Gasolineras Diésel", style = MaterialTheme.typography.titleSmall)
Text(
text = currentAddress,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
maxLines = 1
)
}
}
},
actions = {
if (!isSearchActive && selectedTab != 2) {
IconButton(onClick = { isSearchActive = true }) {
Icon(Icons.Default.Search, contentDescription = "Buscar")
}
}
}
}
)
)
}
},
bottomBar = {
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Default.Place, "Gasolineras") },
label = { Text("Gasolineras") },
label = { Text("Diésel") },
selected = selectedTab == 0,
onClick = { selectedTab = 0 }
)
@@ -203,7 +247,7 @@ fun GasStationApp(repository: GasStationRepository) {
lastLocation?.let { loc ->
isRefreshing = true
scope.launch {
stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true)
stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null)
isRefreshing = false
}
}
@@ -223,22 +267,23 @@ fun GasStationApp(repository: GasStationRepository) {
repository = repository,
lastLocation = lastLocation,
onLogClick = { showFuelDialog = it },
onUpdateStations = { stations = it }
onUpdateStations = { stations = it },
searchQuery = searchQuery
)
}
} else {
val favoriteStations = stations.filter { it.isFavorite }
if (favoriteStations.isEmpty()) {
if (stations.isEmpty()) {
Text("No tienes gasolineras favoritas marcadas", modifier = Modifier.align(Alignment.Center))
} else {
GasStationList(
stations = favoriteStations,
stations = stations,
context = context,
scope = scope,
repository = repository,
lastLocation = lastLocation,
onLogClick = { showFuelDialog = it },
onUpdateStations = { stations = it }
onUpdateStations = { stations = it },
searchQuery = ""
)
}
}
@@ -262,7 +307,6 @@ fun GasStationApp(repository: GasStationRepository) {
showFuelDialog?.let { station ->
FuelLogDialog(
stationName = station.name,
pricePerLiter = station.price,
onDismiss = { showFuelDialog = null },
onConfirm = { liters, total ->
val record = FuelRecord(
@@ -284,7 +328,6 @@ fun GasStationApp(repository: GasStationRepository) {
editRecordDialog?.let { record ->
FuelLogDialog(
stationName = record.stationName,
pricePerLiter = record.pricePerLiter,
initialLiters = record.liters.toString(),
initialTotal = record.totalPrice.toString(),
onDismiss = { editRecordDialog = null },
@@ -311,7 +354,8 @@ fun GasStationList(
repository: GasStationRepository,
lastLocation: android.location.Location?,
onLogClick: (GasStationInfo) -> Unit,
onUpdateStations: (List<GasStationInfo>) -> Unit
onUpdateStations: (List<GasStationInfo>) -> Unit,
searchQuery: String
) {
LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) {
items(stations) { station ->
@@ -325,9 +369,10 @@ fun GasStationList(
onLogClick = { onLogClick(station) },
onFavoriteClick = {
FavoriteStorage.toggleFavorite(context, station.id)
lastLocation?.let { loc ->
val loc = lastLocation ?: LocationStorage.getLastLocation(context)
loc?.let {
scope.launch {
onUpdateStations(repository.getCheapestAndNearest(context, loc))
onUpdateStations(repository.getCheapestAndNearest(context, it, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null))
}
}
}
@@ -386,7 +431,6 @@ fun FuelHistoryScreen(records: List<FuelRecord>, onDelete: (FuelRecord) -> Unit,
@Composable
fun FuelLogDialog(
stationName: String,
pricePerLiter: Double,
initialLiters: String = "",
initialTotal: String = "",
onDismiss: () -> Unit,
@@ -431,6 +475,21 @@ fun FuelLogDialog(
@Composable
fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () -> Unit, onFavoriteClick: () -> Unit) {
val priceColor = when (station.priceCategory) {
PriceCategory.CHEAP -> Color(0xFF2E7D32)
PriceCategory.NORMAL -> MaterialTheme.colorScheme.primary
PriceCategory.EXPENSIVE -> Color(0xFFC62828)
}
val brandColor = when {
station.name.contains("REPSOL", ignoreCase = true) -> Color(0xFFE30613)
station.name.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000)
station.name.contains("BP", ignoreCase = true) -> Color(0xFF00A94F)
station.name.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00)
station.name.contains("SHELL", ignoreCase = true) -> Color(0xFFFFD500)
else -> MaterialTheme.colorScheme.surfaceVariant
}
Card(
modifier = Modifier
.fillMaxWidth()
@@ -446,6 +505,11 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: ()
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
color = brandColor,
modifier = Modifier.size(12.dp).padding(end = 8.dp),
shape = androidx.compose.foundation.shape.CircleShape
) {}
Text(
text = "${station.name} - ${station.municipality}",
style = MaterialTheme.typography.titleMedium,
@@ -471,9 +535,9 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: ()
shape = MaterialTheme.shapes.medium
) {
Text(
text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km",
text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km (recta)",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelLarge,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
@@ -482,14 +546,24 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: ()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.Bottom
) {
Text(
text = "${station.price} €/L",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
)
Column {
Text(
text = "Diésel A: ${station.price} €/L",
style = MaterialTheme.typography.titleLarge,
color = priceColor,
fontWeight = FontWeight.Bold
)
station.pricePremium?.let { premium ->
Text(
text = "Premium: $premium €/L",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
fontWeight = FontWeight.Medium
)
}
}
Button(
onClick = onLogClick,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),