Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
93 changes: 78 additions & 15 deletions app/src/main/java/com/example/myapplication/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = {
Expand All @@ -229,6 +261,7 @@ fun GpsSpooferScreen(modifier: Modifier = Modifier, vm: GpsSpooferViewModel = vi
)
}

// Floating player controls
if (player.isFollowing) {
FloatingPlayerOverlay(
player = player,
Expand All @@ -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")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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 ?: ""
Expand All @@ -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(),
)
}
}
},
Expand All @@ -64,7 +105,7 @@ fun AddEditPointDialog(
}
}) { Text("Save") }
},
dismissButton = { Button(onClick = onDismiss) { Text("Cancel") } }
dismissButton = { OutlinedButton(onClick = onDismiss) { Text("Cancel") } },
)
}

Expand All @@ -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") } },
)
}

Expand All @@ -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") } },
)
}
Loading