diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index b9d904a..6dff85c 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -52,7 +52,9 @@ jobs: - name: Create local.properties if: steps.check.outputs.skip != 'true' - run: echo "sdk.dir=$ANDROID_HOME" > local.properties + run: | + echo "sdk.dir=$ANDROID_HOME" > local.properties + echo "MAPS_API_KEY=DUMMY_CI_KEY" >> local.properties - name: Generate screenshots if: steps.check.outputs.skip != 'true' diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8e3c70c..264c7dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation("androidx.compose.material:material-icons-extended") implementation(libs.maps.compose) implementation(libs.maps.compose.utils) implementation(libs.play.services.cronet) diff --git a/app/src/main/java/com/example/myapplication/MainActivity.kt b/app/src/main/java/com/example/myapplication/MainActivity.kt index 03e9666..415d7eb 100644 --- a/app/src/main/java/com/example/myapplication/MainActivity.kt +++ b/app/src/main/java/com/example/myapplication/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -19,13 +20,26 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.EditLocationAlt import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SheetValue +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState @@ -36,7 +50,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel @@ -136,6 +152,7 @@ fun GpsSpooferScreen(modifier: Modifier = Modifier, vm: GpsSpooferViewModel = vi } ?: emptyList() } + // Dialogs if (vm.showAddDialog) { AddEditPointDialog(null, currentLat, currentLon, cameraState.position.zoom, onDismiss = { vm.showAddDialog = false }) { _, name, lat, lon, zoom -> scope.launch { vm.pointsRepo.add(name, lat, lon, zoom) } @@ -168,12 +185,26 @@ fun GpsSpooferScreen(modifier: Modifier = Modifier, vm: GpsSpooferViewModel = vi modifier = modifier, scaffoldState = scaffoldState, sheetPeekHeight = 128.dp, + sheetContainerColor = MaterialTheme.colorScheme.surface, sheetContent = { - Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilterChip(vm.selectedTab == 0, onClick = { vm.selectedTab = 0 }, label = { Text("Locations") }) - FilterChip(vm.selectedTab == 1, onClick = { vm.selectedTab = 1 }, label = { Text("Routes") }) + Column( + Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + // Segmented button row for tab switching + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { + SegmentedButton( + selected = vm.selectedTab == 0, + onClick = { vm.selectedTab = 0 }, + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), + ) { Text("Locations") } + SegmentedButton( + selected = vm.selectedTab == 1, + onClick = { vm.selectedTab = 1 }, + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), + ) { Text("Routes") } } + if (vm.selectedTab == 0) { LocationsSection(savedPoints, onPointClick = { pt -> vm.latText = pt.latitude.toString() @@ -216,9 +247,10 @@ fun GpsSpooferScreen(modifier: Modifier = Modifier, vm: GpsSpooferViewModel = vi UserLocationPuck(effectiveLat, effectiveLon) } + // Route editing toolbar if (vm.editRoute != null) { RouteEditOverlay( - modifier = Modifier.align(Alignment.TopCenter), + modifier = Modifier.align(Alignment.TopCenter).padding(horizontal = 12.dp, vertical = 8.dp), routeName = vm.editRoute!!.name, nodeCount = vm.editingNodes.size, onSave = { @@ -229,6 +261,7 @@ fun GpsSpooferScreen(modifier: Modifier = Modifier, vm: GpsSpooferViewModel = vi ) } + // Floating player controls if (player.isFollowing) { FloatingPlayerOverlay( player = player, @@ -247,16 +280,46 @@ private fun RouteEditOverlay( onSave: () -> Unit, onCancel: () -> Unit, ) { - Column( - modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface).padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), ) { - Text(routeName, style = MaterialTheme.typography.titleMedium) - Text("Tap to add points, drag to move. $nodeCount point(s).", style = MaterialTheme.typography.bodySmall) - Row(Modifier.fillMaxWidth(), Arrangement.End, Alignment.CenterVertically) { - Button(onClick = onCancel) { Text("Cancel") } - Spacer(Modifier.padding(8.dp)) - Button(onClick = onSave) { Text("Save") } + Column( + Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Outlined.EditLocationAlt, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp), + ) + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Text("Editing: $routeName", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) + Text( + "$nodeCount point${if (nodeCount != 1) "s" else ""} — tap map to add, drag to move", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Row(Modifier.fillMaxWidth(), Arrangement.End, Alignment.CenterVertically, ) { + OutlinedButton(onClick = onCancel) { + Icon(Icons.Default.Close, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Discard") + } + Spacer(Modifier.width(8.dp)) + Button(onClick = onSave) { + Icon(Icons.Default.Check, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Save") + } + } } } } diff --git a/app/src/main/java/com/example/myapplication/ui/components/Dialogs.kt b/app/src/main/java/com/example/myapplication/ui/components/Dialogs.kt index 52b2db2..bf0b447 100644 --- a/app/src/main/java/com/example/myapplication/ui/components/Dialogs.kt +++ b/app/src/main/java/com/example/myapplication/ui/components/Dialogs.kt @@ -3,8 +3,16 @@ package com.example.myapplication.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material.icons.outlined.Route import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -14,6 +22,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.example.myapplication.SavedPoint @@ -30,6 +41,7 @@ fun AddEditPointDialog( var latText by remember { mutableStateOf((point?.latitude ?: currentLat).toString()) } var lonText by remember { mutableStateOf((point?.longitude ?: currentLon).toString()) } var zoomText by remember { mutableStateOf((point?.zoom ?: currentZoom).toString()) } + val focusRequester = remember { FocusRequester() } LaunchedEffect(point) { name = point?.name ?: "" @@ -38,18 +50,47 @@ fun AddEditPointDialog( zoomText = (point?.zoom ?: currentZoom).toString() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + val isNew = point == null AlertDialog( onDismissRequest = onDismiss, + icon = { Icon(Icons.Outlined.LocationOn, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, title = { Text(if (isNew) "Save location" else "Edit location") }, text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField(name, { name = it }, label = { Text("Name") }, singleLine = true, modifier = Modifier.fillMaxWidth()) + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + name, + { name = it }, + label = { Text("Name") }, + placeholder = { Text("e.g. Home, Office...") }, + singleLine = true, + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + ) if (!isNew) { - OutlinedTextField(latText, { latText = it }, label = { Text("Latitude") }, modifier = Modifier.fillMaxWidth()) - OutlinedTextField(lonText, { lonText = it }, label = { Text("Longitude") }, modifier = Modifier.fillMaxWidth()) - OutlinedTextField(zoomText, { zoomText = it }, label = { Text("Zoom (2–22)") }, modifier = Modifier.fillMaxWidth()) + OutlinedTextField( + latText, { latText = it }, + label = { Text("Latitude") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + lonText, { lonText = it }, + label = { Text("Longitude") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + zoomText, { zoomText = it }, + label = { Text("Zoom level") }, + supportingText = { Text("Between 2 and 22") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.fillMaxWidth(), + ) } } }, @@ -64,7 +105,7 @@ fun AddEditPointDialog( } }) { Text("Save") } }, - dismissButton = { Button(onClick = onDismiss) { Text("Cancel") } } + dismissButton = { OutlinedButton(onClick = onDismiss) { Text("Cancel") } }, ) } @@ -76,10 +117,15 @@ fun DeleteConfirmDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Delete location?") }, - text = { Text("Remove \"${point.name}\" from saved locations?") }, - confirmButton = { Button(onClick = { onConfirm(); onDismiss() }) { Text("Delete") } }, - dismissButton = { Button(onClick = onDismiss) { Text("Cancel") } } + title = { Text("Delete \"${point.name}\"?") }, + text = { Text("This location will be permanently removed.") }, + confirmButton = { + Button( + onClick = { onConfirm(); onDismiss() }, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), + ) { Text("Delete") } + }, + dismissButton = { OutlinedButton(onClick = onDismiss) { Text("Cancel") } }, ) } @@ -89,13 +135,25 @@ fun AddRouteDialog( onSave: (name: String) -> Unit, ) { var name by remember { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { focusRequester.requestFocus() } + AlertDialog( onDismissRequest = onDismiss, + icon = { Icon(Icons.Outlined.Route, contentDescription = null, tint = MaterialTheme.colorScheme.primary) }, title = { Text("New route") }, text = { - OutlinedTextField(name, { name = it }, label = { Text("Route name") }, modifier = Modifier.fillMaxWidth()) + OutlinedTextField( + name, + { name = it }, + label = { Text("Route name") }, + placeholder = { Text("e.g. Morning commute...") }, + singleLine = true, + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + ) }, confirmButton = { Button(onClick = { if (name.isNotBlank()) onSave(name.trim()) }) { Text("Create") } }, - dismissButton = { Button(onClick = onDismiss) { Text("Cancel") } } + dismissButton = { OutlinedButton(onClick = onDismiss) { Text("Cancel") } }, ) } diff --git a/app/src/main/java/com/example/myapplication/ui/components/LocationsSheet.kt b/app/src/main/java/com/example/myapplication/ui/components/LocationsSheet.kt index 6b2bcf7..a885cc7 100644 --- a/app/src/main/java/com/example/myapplication/ui/components/LocationsSheet.kt +++ b/app/src/main/java/com/example/myapplication/ui/components/LocationsSheet.kt @@ -6,32 +6,31 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.example.myapplication.R @@ -46,14 +45,39 @@ fun LocationsSection( onDelete: (SavedPoint) -> Unit, ) { Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Saved locations", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(horizontal = 16.dp)) + Text( + "Saved locations", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 16.dp), + ) if (items.isEmpty()) { - Text("Long press on the map to drop a pin.", style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(16.dp)) + Column( + Modifier.fillMaxWidth().padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Outlined.LocationOn, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .5f), + ) + Text( + "No saved locations yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + "Long-press anywhere on the map to drop a pin", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .7f), + ) + } } else { LazyRow( - Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(horizontal = 16.dp) + Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(horizontal = 16.dp), ) { items(items, key = { it.id }) { item -> SavedPointCard(item, onPointClick, onEdit, onDelete) @@ -70,35 +94,55 @@ private fun SavedPointCard( onEdit: (SavedPoint) -> Unit, onDelete: (SavedPoint) -> Unit, ) { - var showMenu by remember { mutableStateOf(false) } - Box { - Column( - Modifier.width(186.dp).height(205.dp) - .clip(MaterialTheme.shapes.extraLarge) - .clickable { onPointClick(point) } - ) { - AsyncImage( - model = staticMapUrl(point.latitude, point.longitude, point.zoom.toInt().coerceIn(1, 22)), - contentDescription = point.name, - modifier = Modifier.fillMaxWidth().height(160.dp), - contentScale = ContentScale.Crop, - placeholder = painterResource(R.drawable.ic_launcher_foreground), - error = painterResource(R.drawable.ic_launcher_foreground), + Card( + modifier = Modifier.width(200.dp), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + onClick = { onPointClick(point) }, + ) { + AsyncImage( + model = staticMapUrl(point.latitude, point.longitude, point.zoom.toInt().coerceIn(1, 22)), + contentDescription = point.name, + modifier = Modifier.fillMaxWidth().height(120.dp), + contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.ic_launcher_foreground), + error = painterResource(R.drawable.ic_launcher_foreground), + ) + + Column(Modifier.fillMaxWidth().padding(start = 12.dp, end = 4.dp, top = 8.dp, bottom = 4.dp)) { + Text( + point.name, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + "%.4f, %.4f".format(point.latitude, point.longitude), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, ) - Box( - Modifier.fillMaxWidth().height(45.dp).background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center, + } + + Row( + Modifier.fillMaxWidth().padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.End, + ) { + IconButton( + onClick = { onEdit(point) }, + modifier = Modifier.size(36.dp), + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onSurfaceVariant), ) { - Text(point.name, style = MaterialTheme.typography.labelMedium, maxLines = 1, modifier = Modifier.padding(horizontal = 8.dp)) + Icon(Icons.Default.Edit, "Edit", modifier = Modifier.size(18.dp)) + } + IconButton( + onClick = { onDelete(point) }, + modifier = Modifier.size(36.dp), + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.error.copy(alpha = .7f)), + ) { + Icon(Icons.Default.Delete, "Delete", modifier = Modifier.size(18.dp)) } - } - IconButton(onClick = { showMenu = true }, Modifier.align(Alignment.TopEnd).padding(4.dp)) { - Icon(Icons.Default.MoreVert, "Options") - } - DropdownMenu(showMenu, onDismissRequest = { showMenu = false }) { - DropdownMenuItem(text = { Text("Use location") }, onClick = { showMenu = false; onPointClick(point) }) - DropdownMenuItem(text = { Text("Edit") }, onClick = { showMenu = false; onEdit(point) }, leadingIcon = { Icon(Icons.Default.Edit, null) }) - DropdownMenuItem(text = { Text("Delete") }, onClick = { showMenu = false; onDelete(point) }, leadingIcon = { Icon(Icons.Default.Delete, null) }) } } } diff --git a/app/src/main/java/com/example/myapplication/ui/components/RoutesSheet.kt b/app/src/main/java/com/example/myapplication/ui/components/RoutesSheet.kt index fe7415f..ac728a5 100644 --- a/app/src/main/java/com/example/myapplication/ui/components/RoutesSheet.kt +++ b/app/src/main/java/com/example/myapplication/ui/components/RoutesSheet.kt @@ -1,38 +1,43 @@ package com.example.myapplication.ui.components -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.outlined.Route import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.example.myapplication.Route import com.example.myapplication.RoutePlayerController @@ -47,15 +52,36 @@ fun RoutesSection( ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { Text("Routes", style = MaterialTheme.typography.titleMedium) - Button(onClick = onAddRoute, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)) { Text("Add route") } + FilledTonalButton(onClick = onAddRoute) { Text("+ New route") } } if (routes.isEmpty()) { - Text("Create a route, then edit it on the map. Tap Follow to start.", style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(vertical = 8.dp)) + Column( + Modifier.fillMaxWidth().padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Outlined.Route, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .5f), + ) + Text( + "No routes yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + "Create a route, draw waypoints on the map, then tap Follow to simulate movement", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .7f), + ) + } } else { routes.forEach { route -> RouteCard( @@ -73,32 +99,70 @@ fun RoutesSection( @Composable fun FloatingPlayerOverlay(player: RoutePlayerController, modifier: Modifier = Modifier) { - Column( - modifier.fillMaxWidth().clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surface).padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + Card( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), ) { - Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { - Text("%.0f km/h".format(player.speedMps * 3.6), style = MaterialTheme.typography.titleMedium) - IconButton(onClick = player::stop) { - Icon(Icons.Default.Close, "Stop") + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + // Header: speed + stop + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Column { + Text("Now following", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + Text( + "%.0f km/h".format(player.speedMps * 3.6), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + } + FilledTonalIconButton(onClick = player::stop) { + Icon(Icons.Default.Close, "Stop") + } } - } - Slider( - value = player.speedMps.toFloat(), - onValueChange = { player.speedMps = it.toDouble() }, - valueRange = 2f..80f, - modifier = Modifier.fillMaxWidth(), - ) - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + + // Speed slider with labels + Column { + Text("Speed", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Slider( + value = player.speedMps.toFloat(), + onValueChange = { player.speedMps = it.toDouble() }, + valueRange = 2f..80f, + modifier = Modifier.fillMaxWidth(), + ) + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween) { + Text("7 km/h", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("288 km/h", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + + // Progress + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Text("Progress", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("%.0f%%".format(player.progress * 100), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Medium) + } + Slider( + player.progress, + onValueChange = player::seek, + valueRange = 0f..1f, + modifier = Modifier.fillMaxWidth(), + ) + } + + // Pause / Resume button Button( onClick = if (player.isPaused) player::resume else player::pause, - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - ) { Text(if (player.isPaused) "Resume" else "Pause") } - Column(Modifier.weight(1f)) { - Slider(player.progress, onValueChange = player::seek, valueRange = 0f..1f, modifier = Modifier.fillMaxWidth()) + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + if (player.isPaused) Icons.Default.PlayArrow else Icons.Default.Close, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text(if (player.isPaused) "Resume" else "Pause") } - Text("%.0f%%".format(player.progress * 100), style = MaterialTheme.typography.labelSmall) } } } @@ -112,30 +176,53 @@ private fun RouteCard( onEdit: () -> Unit, onDelete: () -> Unit, ) { - var showMenu by remember { mutableStateOf(false) } - Box(Modifier.fillMaxWidth()) { - Row( - Modifier.fillMaxWidth().clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .5f)).padding(12.dp), - Arrangement.SpaceBetween, Alignment.CenterVertically, - ) { - Column(Modifier.weight(1f).clickable(onClick = onPreview)) { - Text(route.name, style = MaterialTheme.typography.titleSmall) - Text("${route.waypoints.size} waypoints", style = MaterialTheme.typography.bodySmall) + Card( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.medium, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .6f)), + onClick = onPreview, + ) { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.Top) { + Column(Modifier.weight(1f)) { + Text(route.name, style = MaterialTheme.typography.titleSmall) + Text( + "${route.waypoints.size} waypoint${if (route.waypoints.size != 1) "s" else ""}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + + Row(Modifier.fillMaxWidth(), Arrangement.spacedBy(8.dp)) { if (route.waypoints.size >= 2) { - Button(onClick = onFollow, enabled = !isFollowing, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { - Icon(Icons.Default.PlayArrow, null, Modifier.size(16.dp)) - Text("Follow", Modifier.padding(start = 2.dp)) + Button( + onClick = onFollow, + enabled = !isFollowing, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + ) { + Icon(Icons.Default.PlayArrow, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Follow") } } - IconButton(onClick = { showMenu = true }) { Icon(Icons.Default.MoreVert, "Options") } + OutlinedButton( + onClick = onEdit, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + ) { + Icon(Icons.Default.Edit, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Edit") + } + IconButton( + onClick = onDelete, + modifier = Modifier.size(40.dp), + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.error.copy(alpha = .7f)), + ) { + Icon(Icons.Default.Delete, "Delete", Modifier.size(20.dp)) + } } } - DropdownMenu(showMenu, onDismissRequest = { showMenu = false }) { - DropdownMenuItem(text = { Text("Edit") }, onClick = { showMenu = false; onEdit() }) - DropdownMenuItem(text = { Text("Delete") }, onClick = { showMenu = false; onDelete() }, leadingIcon = { Icon(Icons.Default.Delete, null) }) - } } } diff --git a/app/src/main/java/com/example/myapplication/ui/theme/Color.kt b/app/src/main/java/com/example/myapplication/ui/theme/Color.kt index 1ce3e5d..ac1d3ee 100644 --- a/app/src/main/java/com/example/myapplication/ui/theme/Color.kt +++ b/app/src/main/java/com/example/myapplication/ui/theme/Color.kt @@ -2,10 +2,11 @@ package com.example.myapplication.ui.theme import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) +// Light teal/green palette — earthy, map-appropriate +val Teal80 = Color(0xFFA0D5C8) +val TealGrey80 = Color(0xFFB8C9C4) +val Sand80 = Color(0xFFD6C9A8) -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Teal40 = Color(0xFF00796B) +val TealGrey40 = Color(0xFF4A635C) +val Sand40 = Color(0xFF8D7B4E) diff --git a/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt b/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt index 174f73f..b0e5721 100644 --- a/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package com.example.myapplication.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -9,43 +8,41 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 + primary = Teal80, + secondary = TealGrey80, + tertiary = Sand80, + surface = Color(0xFF1A1C1E), + surfaceVariant = Color(0xFF2A2F2D), + onSurface = Color(0xFFE2E3DE), + onSurfaceVariant = Color(0xFFC0C9C3), ) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + primary = Teal40, + secondary = TealGrey40, + tertiary = Sand40, + surface = Color(0xFFF8FAF7), + surfaceVariant = Color(0xFFE8EDEA), + onSurface = Color(0xFF1A1C1E), + onSurfaceVariant = Color(0xFF414942), + error = Color(0xFFBA1A1A), ) @Composable fun MyApplicationTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme else -> LightColorScheme } @@ -53,6 +50,6 @@ fun MyApplicationTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } \ No newline at end of file diff --git a/app/src/screenshotTest/kotlin/com/example/myapplication/ScreenshotTests.kt b/app/src/screenshotTest/kotlin/com/example/myapplication/ScreenshotTests.kt index 773ed9e..1096043 100644 --- a/app/src/screenshotTest/kotlin/com/example/myapplication/ScreenshotTests.kt +++ b/app/src/screenshotTest/kotlin/com/example/myapplication/ScreenshotTests.kt @@ -1,26 +1,41 @@ package com.example.myapplication -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.outlined.EditLocationAlt +import androidx.compose.material.icons.outlined.Route import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilterChip +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.tools.screenshot.PreviewTest @@ -32,15 +47,16 @@ import com.example.myapplication.ui.theme.MyApplicationTheme // ── Locations tab ──────────────────────────────────────────────────── +@OptIn(ExperimentalMaterial3Api::class) @PreviewTest @Preview(showBackground = true, widthDp = 400) @Composable fun LocationsTabEmptyPreview() { MyApplicationTheme(dynamicColor = false) { Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilterChip(selected = true, onClick = {}, label = { Text("Locations") }) - FilterChip(selected = false, onClick = {}, label = { Text("Routes") }) + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { + SegmentedButton(selected = true, onClick = {}, shape = SegmentedButtonDefaults.itemShape(0, 2)) { Text("Locations") } + SegmentedButton(selected = false, onClick = {}, shape = SegmentedButtonDefaults.itemShape(1, 2)) { Text("Routes") } } LocationsSection( items = emptyList(), @@ -52,15 +68,16 @@ fun LocationsTabEmptyPreview() { } } +@OptIn(ExperimentalMaterial3Api::class) @PreviewTest @Preview(showBackground = true, widthDp = 400) @Composable fun LocationsTabWithItemsPreview() { MyApplicationTheme(dynamicColor = false) { Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilterChip(selected = true, onClick = {}, label = { Text("Locations") }) - FilterChip(selected = false, onClick = {}, label = { Text("Routes") }) + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { + SegmentedButton(selected = true, onClick = {}, shape = SegmentedButtonDefaults.itemShape(0, 2)) { Text("Locations") } + SegmentedButton(selected = false, onClick = {}, shape = SegmentedButtonDefaults.itemShape(1, 2)) { Text("Routes") } } LocationsSection( items = listOf( @@ -78,54 +95,57 @@ fun LocationsTabWithItemsPreview() { // ── Routes tab ─────────────────────────────────────────────────────── +@OptIn(ExperimentalMaterial3Api::class) @PreviewTest @Preview(showBackground = true, widthDp = 400) @Composable fun RoutesTabEmptyPreview() { MyApplicationTheme(dynamicColor = false) { Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilterChip(selected = false, onClick = {}, label = { Text("Locations") }) - FilterChip(selected = true, onClick = {}, label = { Text("Routes") }) + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { + SegmentedButton(selected = false, onClick = {}, shape = SegmentedButtonDefaults.itemShape(0, 2)) { Text("Locations") } + SegmentedButton(selected = true, onClick = {}, shape = SegmentedButtonDefaults.itemShape(1, 2)) { Text("Routes") } } Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { Text("Routes", style = MaterialTheme.typography.titleMedium) - Button(onClick = {}, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)) { - Text("Add route") - } + FilledTonalButton(onClick = {}) { Text("+ New route") } } - Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text("Speed: 25 m/s (90 km/h)", style = MaterialTheme.typography.labelMedium) - Slider(value = 25f, onValueChange = {}, valueRange = 2f..80f, modifier = Modifier.fillMaxWidth()) + Column( + Modifier.fillMaxWidth().padding(vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Outlined.Route, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .5f), + ) + Text("No routes yet", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + "Create a route, draw waypoints on the map, then tap Follow to simulate movement", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .7f), + ) } - Text( - "Add a route, then edit it on the map. Tap Follow to simulate movement.", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(vertical = 8.dp), - ) } } } +@OptIn(ExperimentalMaterial3Api::class) @PreviewTest @Preview(showBackground = true, widthDp = 400) @Composable fun RoutesTabWithRoutesPreview() { MyApplicationTheme(dynamicColor = false) { Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilterChip(selected = false, onClick = {}, label = { Text("Locations") }) - FilterChip(selected = true, onClick = {}, label = { Text("Routes") }) + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { + SegmentedButton(selected = false, onClick = {}, shape = SegmentedButtonDefaults.itemShape(0, 2)) { Text("Locations") } + SegmentedButton(selected = true, onClick = {}, shape = SegmentedButtonDefaults.itemShape(1, 2)) { Text("Routes") } } Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { Text("Routes", style = MaterialTheme.typography.titleMedium) - Button(onClick = {}, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)) { - Text("Add route") - } - } - Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text("Speed: 25 m/s (90 km/h)", style = MaterialTheme.typography.labelMedium) - Slider(value = 25f, onValueChange = {}, valueRange = 2f..80f, modifier = Modifier.fillMaxWidth()) + FilledTonalButton(onClick = {}) { Text("+ New route") } } // Sample route cards RouteCardPreview("Morning Commute", 5) @@ -137,21 +157,33 @@ fun RoutesTabWithRoutesPreview() { @Composable private fun RouteCardPreview(name: String, waypointCount: Int) { - Row( - Modifier.fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .5f)) - .padding(12.dp), - Arrangement.SpaceBetween, - Alignment.CenterVertically, + Card( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.medium, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .6f)), ) { - Column(Modifier.weight(1f)) { - Text(name, style = MaterialTheme.typography.titleSmall) - Text("$waypointCount waypoints", style = MaterialTheme.typography.bodySmall) - } - Button(onClick = {}, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { - Icon(Icons.Default.PlayArrow, null, Modifier.padding(end = 2.dp)) - Text("Follow") + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column { + Text(name, style = MaterialTheme.typography.titleSmall) + Text("$waypointCount waypoints", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Row(Modifier.fillMaxWidth(), Arrangement.spacedBy(8.dp)) { + Button(onClick = {}, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.PlayArrow, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Follow") + } + OutlinedButton(onClick = {}) { + Icon(Icons.Default.Edit, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Edit") + } + IconButton( + onClick = {}, + modifier = Modifier.size(40.dp), + colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.error.copy(alpha = .7f)), + ) { Icon(Icons.Default.Delete, "Delete", Modifier.size(20.dp)) } + } } } } @@ -163,33 +195,40 @@ private fun RouteCardPreview(name: String, waypointCount: Int) { @Composable fun RoutePlayerControlsPreview() { MyApplicationTheme(dynamicColor = false) { - Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Column( - Modifier.fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text("Route player", style = MaterialTheme.typography.titleSmall) - Row( - Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button(onClick = {}, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)) { - Text("Pause") + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), + ) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Column { + Text("Now following", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + Text("90 km/h", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) } - Column(Modifier.weight(1f)) { - Slider(value = 0.42f, onValueChange = {}, valueRange = 0f..1f, modifier = Modifier.fillMaxWidth()) - Text("42%", style = MaterialTheme.typography.labelSmall) + FilledTonalIconButton(onClick = {}) { Icon(Icons.Default.Close, "Stop") } + } + Column { + Text("Speed", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Slider(value = 25f, onValueChange = {}, valueRange = 2f..80f, modifier = Modifier.fillMaxWidth()) + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween) { + Text("7 km/h", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("288 km/h", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } } - Button( - onClick = {}, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - modifier = Modifier.fillMaxWidth(), - ) { Text("Stop") } + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Text("Progress", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text("42%", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Medium) + } + Slider(value = 0.42f, onValueChange = {}, valueRange = 0f..1f, modifier = Modifier.fillMaxWidth()) + } + Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { + Icon(Icons.Default.Close, null, Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text("Pause") + } } } } @@ -202,17 +241,33 @@ fun RoutePlayerControlsPreview() { @Composable fun RouteEditOverlayPreview() { MyApplicationTheme(dynamicColor = false) { - Box(Modifier.fillMaxWidth()) { - Column( - Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface).padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text("Morning Commute", style = MaterialTheme.typography.titleMedium) - Text("Tap map to add points, drag nodes to move. 5 point(s).", style = MaterialTheme.typography.bodySmall) + Card( + modifier = Modifier.fillMaxWidth().padding(12.dp), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + ) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Outlined.EditLocationAlt, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp)) + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Text("Editing: Morning Commute", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) + Text("5 points — tap map to add, drag to move", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } Row(Modifier.fillMaxWidth(), Arrangement.End, Alignment.CenterVertically) { - Button(onClick = {}) { Text("Cancel") } - Box(Modifier.padding(8.dp)) - Button(onClick = {}) { Text("Save") } + OutlinedButton(onClick = {}) { + Icon(Icons.Default.Close, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Discard") + } + Spacer(Modifier.width(8.dp)) + Button(onClick = {}) { + Icon(Icons.Default.Check, null, Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Save") + } } } } diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/AddLocationDialogPreview_748aa731_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/AddLocationDialogPreview_748aa731_0.png new file mode 100644 index 0000000..3f5151c Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/AddLocationDialogPreview_748aa731_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/DeleteLocationDialogPreview_748aa731_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/DeleteLocationDialogPreview_748aa731_0.png new file mode 100644 index 0000000..de6fb8b Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/DeleteLocationDialogPreview_748aa731_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/EditLocationDialogPreview_748aa731_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/EditLocationDialogPreview_748aa731_0.png new file mode 100644 index 0000000..73deac1 Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/EditLocationDialogPreview_748aa731_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/LocationsTabEmptyPreview_e7711029_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/LocationsTabEmptyPreview_e7711029_0.png new file mode 100644 index 0000000..5938b50 Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/LocationsTabEmptyPreview_e7711029_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/LocationsTabWithItemsPreview_e7711029_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/LocationsTabWithItemsPreview_e7711029_0.png new file mode 100644 index 0000000..41ada28 Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/LocationsTabWithItemsPreview_e7711029_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/NewRouteDialogPreview_748aa731_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/NewRouteDialogPreview_748aa731_0.png new file mode 100644 index 0000000..b4e68fd Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/NewRouteDialogPreview_748aa731_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RouteEditOverlayPreview_e7711029_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RouteEditOverlayPreview_e7711029_0.png new file mode 100644 index 0000000..75a7eb2 Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RouteEditOverlayPreview_e7711029_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutePlayerControlsPreview_e7711029_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutePlayerControlsPreview_e7711029_0.png new file mode 100644 index 0000000..a0d84f7 Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutePlayerControlsPreview_e7711029_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutesTabEmptyPreview_e7711029_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutesTabEmptyPreview_e7711029_0.png new file mode 100644 index 0000000..8c9facd Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutesTabEmptyPreview_e7711029_0.png differ diff --git a/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutesTabWithRoutesPreview_e7711029_0.png b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutesTabWithRoutesPreview_e7711029_0.png new file mode 100644 index 0000000..b260af1 Binary files /dev/null and b/app/src/screenshotTestDebug/reference/com/example/myapplication/ScreenshotTestsKt/RoutesTabWithRoutesPreview_e7711029_0.png differ