v0.5.1 — UI: map ring, ⌖ crosshair, source-color dots, bottom START
Build APK / build (push) Waiting to run
Build APK / build (push) Waiting to run
- Larger main-screen map circle (220 -> 300dp) wrapped in a threat-color ring (green/yellow/orange/red while scanning, gray idle). - User position renders as a ⌖ crosshair; map geodata color-coded by source (Flock/DeFlock red, Waze blue, Citizen purple) in both the main circle and the floating overlay bubble. - START button moved to the bottom of the screen. - README brought current: COMMERCIAL source + overlay documented, Waze-via-proxy and the CI release pipeline described, architecture/permissions/changelog updated, build reqs corrected to JDK 17 / SDK 35. versionCode 16 -> 17, versionName 0.5.0 -> 0.5.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
This commit is contained in:
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "org.soulstone.overwatch"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 16
|
||||
versionName = "0.5.0"
|
||||
versionCode = 17
|
||||
versionName = "0.5.1"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -135,6 +136,7 @@ fun MainScreen(
|
||||
animating = running,
|
||||
userLocation = userLocation,
|
||||
mapPoints = mapPoints,
|
||||
events = events,
|
||||
mapRadiusMeters = mapRadiusMeters,
|
||||
onTap = { showSheet = true }
|
||||
)
|
||||
@@ -150,9 +152,15 @@ fun MainScreen(
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
StatusText(running = running, threat = threat, score = score, events = events)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
// Push the primary action button to the bottom of the screen.
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Button(
|
||||
onClick = onStartStop,
|
||||
enabled = canStart,
|
||||
@@ -200,6 +208,8 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
if (showSheet) {
|
||||
@@ -230,6 +240,7 @@ private fun ThreatMapCircle(
|
||||
animating: Boolean,
|
||||
userLocation: Location?,
|
||||
mapPoints: List<DeflockClient.AlprPoint>,
|
||||
events: List<DetectionEvent>,
|
||||
mapRadiusMeters: Float,
|
||||
onTap: () -> Unit
|
||||
) {
|
||||
@@ -240,6 +251,9 @@ private fun ThreatMapCircle(
|
||||
ThreatLevel.ORANGE -> ThreatColors.Orange
|
||||
ThreatLevel.RED -> ThreatColors.Red
|
||||
}
|
||||
// Steady threat-tier ring around the circle: tier color while scanning,
|
||||
// muted gray when idle. Gives the current level at a glance even over the map.
|
||||
val ringColor = if (animating) activeColor else idleColor
|
||||
|
||||
val transition = rememberInfiniteTransition(label = "pulse")
|
||||
val pulse by transition.animateFloat(
|
||||
@@ -254,7 +268,9 @@ private fun ThreatMapCircle(
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(220.dp)
|
||||
.size(300.dp)
|
||||
.border(width = 6.dp, color = ringColor, shape = CircleShape)
|
||||
.padding(6.dp)
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -300,8 +316,10 @@ private fun ThreatMapCircle(
|
||||
val ctx = LocalContext.current
|
||||
// Build the marker drawables once per Composition rather than
|
||||
// every recomposition — bitmap allocation isn't free.
|
||||
val userDot = remember(ctx) { dotDrawable(ctx.resources, 36, DOT_USER_BLUE) }
|
||||
val userMark = remember(ctx) { crosshairDrawable(ctx.resources, 46, MARK_USER_WHITE) }
|
||||
val flockDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_FLOCK_RED) }
|
||||
val wazeDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_WAZE_BLUE) }
|
||||
val citizenDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_CITIZEN_PURPLE) }
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { c ->
|
||||
@@ -317,7 +335,8 @@ private fun ThreatMapCircle(
|
||||
map.controller.setCenter(GeoPoint(fix.latitude, fix.longitude))
|
||||
map.overlays.clear()
|
||||
|
||||
// ALPR dots first, user dot last so the user draws on top.
|
||||
// Source dots first (Flock red, Waze blue, Citizen purple),
|
||||
// user position last so the crosshair always draws on top.
|
||||
for (p in mapPoints) {
|
||||
map.overlays.add(
|
||||
Marker(map).apply {
|
||||
@@ -329,11 +348,29 @@ private fun ThreatMapCircle(
|
||||
}
|
||||
)
|
||||
}
|
||||
for (e in events) {
|
||||
val lat = e.lat ?: continue
|
||||
val lon = e.lon ?: continue
|
||||
val dot = when (e.source) {
|
||||
DetectionSource.WAZE -> wazeDot
|
||||
DetectionSource.CITIZEN -> citizenDot
|
||||
else -> null
|
||||
} ?: continue
|
||||
map.overlays.add(
|
||||
Marker(map).apply {
|
||||
position = GeoPoint(lat, lon)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = dot
|
||||
title = e.label
|
||||
setInfoWindow(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
map.overlays.add(
|
||||
Marker(map).apply {
|
||||
position = GeoPoint(fix.latitude, fix.longitude)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = userDot
|
||||
icon = userMark
|
||||
setInfoWindow(null)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,5 +25,44 @@ internal fun dotDrawable(
|
||||
return BitmapDrawable(resources, bitmap)
|
||||
}
|
||||
|
||||
internal const val DOT_USER_BLUE = 0xFF2196F3.toInt()
|
||||
internal const val DOT_FLOCK_RED = 0xFFD7263D.toInt()
|
||||
/** Builds a ⌖-style position mark (register / gun-sight): a stroked circle with
|
||||
* a cross through it, painted over a dark halo so it reads on any map tile. Used
|
||||
* for the user's own location so it stays distinct from the colored source dots. */
|
||||
internal fun crosshairDrawable(
|
||||
resources: Resources,
|
||||
sizePx: Int,
|
||||
markColor: Int
|
||||
): BitmapDrawable {
|
||||
val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val c = sizePx / 2f
|
||||
val r = sizePx * 0.30f
|
||||
val pad = sizePx * 0.07f
|
||||
val stroke = sizePx * 0.10f
|
||||
|
||||
val halo = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
color = 0xB3000000.toInt()
|
||||
strokeWidth = stroke + 3f
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
val mark = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
color = markColor
|
||||
strokeWidth = stroke
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
// Halo pass then color pass so the mark stays legible on light or dark tiles.
|
||||
for (p in listOf(halo, mark)) {
|
||||
canvas.drawCircle(c, c, r, p)
|
||||
canvas.drawLine(pad, c, sizePx - pad, c, p) // horizontal
|
||||
canvas.drawLine(c, pad, c, sizePx - pad, p) // vertical
|
||||
}
|
||||
return BitmapDrawable(resources, bitmap)
|
||||
}
|
||||
|
||||
// Per-source marker colors so geodata reads at a glance.
|
||||
internal const val DOT_FLOCK_RED = 0xFFD7263D.toInt() // Flock / DeFlock ALPR cameras
|
||||
internal const val DOT_WAZE_BLUE = 0xFF2196F3.toInt() // Waze police reports
|
||||
internal const val DOT_CITIZEN_PURPLE = 0xFF9C27B0.toInt() // Citizen incidents
|
||||
internal const val MARK_USER_WHITE = 0xFFFFFFFF.toInt() // the user's own position (⌖)
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.soulstone.overwatch.data.settings.Settings
|
||||
import org.soulstone.overwatch.fusion.DetectionSource
|
||||
import org.soulstone.overwatch.fusion.ThreatLevel
|
||||
import org.soulstone.overwatch.service.DetectionService
|
||||
import org.soulstone.overwatch.ui.theme.ThreatColors
|
||||
@@ -54,9 +55,11 @@ fun OverlayBubble() {
|
||||
val threat by DetectionService.store.threatLevel.collectAsState()
|
||||
val userLocation by DetectionService.location.collectAsState()
|
||||
val mapPoints by DetectionService.mapPoints.collectAsState()
|
||||
val events by DetectionService.store.events.collectAsState()
|
||||
val deflockProx by settings.deflockProximityM.collectAsState()
|
||||
val citizenProx by settings.citizenProximityM.collectAsState()
|
||||
val radius = max(deflockProx, citizenProx).toFloat()
|
||||
val wazeProx by settings.wazeProximityM.collectAsState()
|
||||
val radius = max(max(deflockProx, citizenProx), wazeProx).toFloat()
|
||||
|
||||
val activeColor = when (threat) {
|
||||
ThreatLevel.GREEN -> ThreatColors.Green
|
||||
@@ -76,8 +79,10 @@ fun OverlayBubble() {
|
||||
label = "overlay-pulse"
|
||||
)
|
||||
|
||||
val userDot = remember(ctx) { dotDrawable(ctx.resources, 30, DOT_USER_BLUE) }
|
||||
val userMark = remember(ctx) { crosshairDrawable(ctx.resources, 34, MARK_USER_WHITE) }
|
||||
val flockDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_FLOCK_RED) }
|
||||
val wazeDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_WAZE_BLUE) }
|
||||
val citizenDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_CITIZEN_PURPLE) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -128,11 +133,28 @@ fun OverlayBubble() {
|
||||
}
|
||||
)
|
||||
}
|
||||
for (e in events) {
|
||||
val lat = e.lat ?: continue
|
||||
val lon = e.lon ?: continue
|
||||
val dot = when (e.source) {
|
||||
DetectionSource.WAZE -> wazeDot
|
||||
DetectionSource.CITIZEN -> citizenDot
|
||||
else -> null
|
||||
} ?: continue
|
||||
map.overlays.add(
|
||||
Marker(map).apply {
|
||||
position = GeoPoint(lat, lon)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = dot
|
||||
setInfoWindow(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
map.overlays.add(
|
||||
Marker(map).apply {
|
||||
position = GeoPoint(fix.latitude, fix.longitude)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = userDot
|
||||
icon = userMark
|
||||
setInfoWindow(null)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user