Polish UI + freshen core deps
- Rewrote MainScreen: top bar, prominent status hero (idle/clear/watching/alert), active vs trusted sections, per-tracker cards with status icons, and a detail bottom sheet with co-movement evidence + trust action. - Sensitivity selector with per-mode explanation; on-device/no-network footer. - core-ktx 1.13.1->1.15.0, activity-compose 1.9.3->1.10.1 (kept the proven Kotlin/AGP/Compose matrix for build stability). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
This commit is contained in:
@@ -1,31 +1,60 @@
|
|||||||
package org.soulstone.vigil.ui
|
package org.soulstone.vigil.ui
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
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.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bluetooth
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.GppMaybe
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.Radar
|
||||||
|
import androidx.compose.material.icons.filled.Shield
|
||||||
|
import androidx.compose.material.icons.filled.Verified
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
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 org.soulstone.vigil.BuildConfig
|
||||||
import org.soulstone.vigil.data.db.TrackerEntity
|
import org.soulstone.vigil.data.db.TrackerEntity
|
||||||
import org.soulstone.vigil.model.Sensitivity
|
import org.soulstone.vigil.model.Sensitivity
|
||||||
import org.soulstone.vigil.model.TrackerEcosystem
|
import org.soulstone.vigil.model.TrackerEcosystem
|
||||||
@@ -34,6 +63,7 @@ import org.soulstone.vigil.ui.theme.VigilGreen
|
|||||||
import org.soulstone.vigil.ui.theme.VigilPeach
|
import org.soulstone.vigil.ui.theme.VigilPeach
|
||||||
import org.soulstone.vigil.ui.theme.VigilRed
|
import org.soulstone.vigil.ui.theme.VigilRed
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
running: Boolean,
|
running: Boolean,
|
||||||
@@ -44,98 +74,231 @@ fun MainScreen(
|
|||||||
onSetSensitivity: (Sensitivity) -> Unit,
|
onSetSensitivity: (Sensitivity) -> Unit,
|
||||||
onApprove: (String, Boolean) -> Unit
|
onApprove: (String, Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val alerting = trackers.count { statusOf(it) == TrackerStatus.ALERTING }
|
var detail by remember { mutableStateOf<TrackerEntity?>(null) }
|
||||||
val suspicious = trackers.count { statusOf(it) == TrackerStatus.SUSPICIOUS }
|
|
||||||
|
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
val active = trackers.filter { !isTrusted(it) }.sortedByDescending { riskRank(it) }
|
||||||
Column(Modifier.fillMaxSize().padding(16.dp)) {
|
val trusted = trackers.filter { isTrusted(it) }
|
||||||
Text("VIGIL", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
val alerting = active.count { statusOf(it) == TrackerStatus.ALERTING }
|
||||||
|
val suspicious = active.count { statusOf(it) == TrackerStatus.SUSPICIOUS }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Filled.Shield, null, tint = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(Modifier.size(8.dp))
|
||||||
|
Text("VIGIL", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Lock, null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(4.dp))
|
||||||
Text(
|
Text(
|
||||||
"what's been following you",
|
"on-device",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { pad ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(pad).padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
item { Spacer(Modifier.height(4.dp)) }
|
||||||
|
item { StatusHero(running, alerting, suspicious) }
|
||||||
|
item {
|
||||||
|
Button(
|
||||||
|
onClick = onStartStop,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (running) MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
else MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = if (running) MaterialTheme.colorScheme.onSurface
|
||||||
|
else MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(if (running) Icons.Filled.Radar else Icons.Filled.Shield, null)
|
||||||
|
Spacer(Modifier.size(8.dp))
|
||||||
|
Text(if (running) "STOP WATCHING" else "START WATCHING", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permissionMessage?.let {
|
||||||
|
item {
|
||||||
|
Text(it, style = MaterialTheme.typography.bodySmall, color = VigilPeach)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item { SensitivitySelector(sensitivity, onSetSensitivity) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
if (active.isEmpty()) "Nothing following you" else "Active (${active.size})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (active.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
if (running) "Listening… no trackers are moving with you."
|
||||||
|
else "Not scanning. Tap Start Watching.",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
}
|
||||||
|
} else {
|
||||||
|
items(active, key = { it.stableId }) { t ->
|
||||||
|
TrackerCard(t, onClick = { detail = t }, onApprove = onApprove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StatusBanner(running, alerting, suspicious)
|
if (trusted.isNotEmpty()) {
|
||||||
Spacer(Modifier.height(16.dp))
|
item { SectionHeader("Trusted (${trusted.size})") }
|
||||||
|
items(trusted, key = { it.stableId }) { t ->
|
||||||
|
TrackerCard(t, onClick = { detail = t }, onApprove = onApprove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button(
|
item {
|
||||||
onClick = onStartStop,
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
"VIGIL ${BuildConfig.VERSION_NAME} · no network, no accounts, no data leaves this phone",
|
||||||
colors = ButtonDefaults.buttonColors(
|
style = MaterialTheme.typography.labelSmall,
|
||||||
containerColor = if (running) MaterialTheme.colorScheme.surfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
else MaterialTheme.colorScheme.primary
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
Text(if (running) "STOP WATCHING" else "START WATCHING")
|
|
||||||
}
|
}
|
||||||
permissionMessage?.let {
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text(it, style = MaterialTheme.typography.bodySmall, color = VigilPeach)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(20.dp))
|
detail?.let { t ->
|
||||||
Text("Sensitivity", style = MaterialTheme.typography.labelLarge)
|
val sheetState = rememberModalBottomSheetState()
|
||||||
Spacer(Modifier.height(6.dp))
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { detail = null },
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
TrackerDetail(t, onApprove = { id, a -> onApprove(id, a); detail = null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusHero(running: Boolean, alerting: Int, suspicious: Int) {
|
||||||
|
data class Look(val title: String, val subtitle: String, val icon: ImageVector, val color: Color)
|
||||||
|
|
||||||
|
val look = when {
|
||||||
|
!running -> Look("Idle", "Tap Start Watching to begin.", Icons.Filled.Shield, MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
alerting > 0 -> Look(
|
||||||
|
if (alerting == 1) "A tracker is following you" else "$alerting trackers following you",
|
||||||
|
"Moving with you across places and time.", Icons.Filled.Warning, VigilRed
|
||||||
|
)
|
||||||
|
suspicious > 0 -> Look(
|
||||||
|
"Keeping watch",
|
||||||
|
"$suspicious possible follower(s) — not yet confirmed.", Icons.Filled.GppMaybe, VigilPeach
|
||||||
|
)
|
||||||
|
else -> Look("You're clear", "Nothing has been following you.", Icons.Filled.CheckCircle, VigilGreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = look.color)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier.size(56.dp).clip(CircleShape).background(Color(0x22111111)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(look.icon, null, tint = Color(0xFF11111B), modifier = Modifier.size(32.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
look.title,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color(0xFF11111B)
|
||||||
|
)
|
||||||
|
Text(look.subtitle, style = MaterialTheme.typography.bodyMedium, color = Color(0xCC11111B))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SensitivitySelector(current: Sensitivity, onSet: (Sensitivity) -> Unit) {
|
||||||
|
Column {
|
||||||
|
SectionHeader("Sensitivity")
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Sensitivity.entries.forEach { s ->
|
Sensitivity.entries.forEach { s ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = s == sensitivity,
|
selected = s == current,
|
||||||
onClick = { onSetSensitivity(s) },
|
onClick = { onSet(s) },
|
||||||
label = { Text(s.name.lowercase().replaceFirstChar { it.uppercase() }) }
|
label = { Text(s.name.lowercase().replaceFirstChar { it.uppercase() }) },
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
selectedLabelColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
Spacer(Modifier.height(20.dp))
|
|
||||||
Text("Trackers seen (${trackers.size})", style = MaterialTheme.typography.labelLarge)
|
|
||||||
Spacer(Modifier.height(6.dp))
|
|
||||||
if (trackers.isEmpty()) {
|
|
||||||
Text(
|
Text(
|
||||||
if (running) "Listening… nothing near you yet." else "Not scanning.",
|
when (current) {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
Sensitivity.HIGH -> "Alerts fastest — more early warnings, more false alarms."
|
||||||
|
Sensitivity.MEDIUM -> "Balanced — the recommended default."
|
||||||
|
Sensitivity.LOW -> "Alerts only on strong, sustained evidence."
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
items(trackers, key = { it.stableId }) { t -> TrackerRow(t, onApprove) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatusBanner(running: Boolean, alerting: Int, suspicious: Int) {
|
private fun SectionHeader(text: String) {
|
||||||
val (label, color) = when {
|
|
||||||
!running -> "Idle" to MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
alerting > 0 -> "$alerting tracker(s) following you" to VigilRed
|
|
||||||
suspicious > 0 -> "Watching $suspicious possible follower(s)" to VigilPeach
|
|
||||||
else -> "Clear — nothing is following you" to VigilGreen
|
|
||||||
}
|
|
||||||
Card(
|
|
||||||
Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = color)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
label,
|
text.uppercase(),
|
||||||
Modifier.padding(16.dp),
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color(0xFF11111B),
|
fontWeight = FontWeight.Bold,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
letterSpacing = 1.sp,
|
||||||
fontWeight = FontWeight.Bold
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrackerRow(t: TrackerEntity, onApprove: (String, Boolean) -> Unit) {
|
private fun TrackerCard(t: TrackerEntity, onClick: () -> Unit, onApprove: (String, Boolean) -> Unit) {
|
||||||
val status = statusOf(t)
|
val status = statusOf(t)
|
||||||
Card(Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) {
|
val accent = statusColor(status)
|
||||||
Row(
|
Card(
|
||||||
Modifier.fillMaxWidth().padding(12.dp),
|
Modifier.fillMaxWidth().clickable(onClick = onClick),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Box(
|
||||||
|
Modifier.size(40.dp).clip(CircleShape).background(accent.copy(alpha = 0.18f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(statusIcon(status), null, tint = accent, modifier = Modifier.size(22.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(12.dp))
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(ecosystemDisplay(t.ecosystem), fontWeight = FontWeight.SemiBold)
|
Text(ecosystemDisplay(t.ecosystem), fontWeight = FontWeight.SemiBold)
|
||||||
Text(
|
Text(
|
||||||
@@ -144,15 +307,79 @@ private fun TrackerRow(t: TrackerEntity, onApprove: (String, Boolean) -> Unit) {
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
when (status) {
|
||||||
|
TrackerStatus.SAFE_APPROVED ->
|
||||||
|
TextButton(onClick = { onApprove(t.stableId, false) }) { Text("Undo") }
|
||||||
|
TrackerStatus.SAFE_BASELINE -> Icon(
|
||||||
|
Icons.Filled.Verified, null,
|
||||||
|
tint = VigilGreen, modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
else -> TextButton(onClick = { onApprove(t.stableId, true) }) { Text("It's mine") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TrackerDetail(t: TrackerEntity, onApprove: (String, Boolean) -> Unit) {
|
||||||
|
val status = statusOf(t)
|
||||||
|
Column(Modifier.fillMaxWidth().padding(24.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Filled.Bluetooth, null, tint = statusColor(status))
|
||||||
|
Spacer(Modifier.size(10.dp))
|
||||||
|
Text(ecosystemDisplay(t.ecosystem), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(statusLabel(status), color = statusColor(status), fontWeight = FontWeight.SemiBold)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
DetailRow("Sightings", t.sightingCount.toString())
|
||||||
|
DetailRow("First seen", relative(t.firstSeen))
|
||||||
|
DetailRow("Last seen", relative(t.lastSeen))
|
||||||
|
if (t.anchorDayCount > 0) {
|
||||||
|
DetailRow("Days seen at your places", "${t.anchorDayCount} (trusted at 3)")
|
||||||
|
}
|
||||||
|
DetailRow("Identity", t.stableId.take(22) + "…")
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
explanation(status),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
if (status == TrackerStatus.SAFE_APPROVED) {
|
if (status == TrackerStatus.SAFE_APPROVED) {
|
||||||
TextButton(onClick = { onApprove(t.stableId, false) }) { Text("Un-approve") }
|
Button(onClick = { onApprove(t.stableId, false) }, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Remove from approved")
|
||||||
|
}
|
||||||
} else if (status != TrackerStatus.SAFE_BASELINE) {
|
} else if (status != TrackerStatus.SAFE_BASELINE) {
|
||||||
TextButton(onClick = { onApprove(t.stableId, true) }) { Text("This is mine") }
|
Button(onClick = { onApprove(t.stableId, true) }, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("This is mine — stop alerting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DetailRow(label: String, value: String) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Text(value, fontWeight = FontWeight.Medium, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- pure helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
private fun isTrusted(t: TrackerEntity) = t.approved || t.baselineSafe
|
||||||
|
|
||||||
|
private fun riskRank(t: TrackerEntity): Int = when (statusOf(t)) {
|
||||||
|
TrackerStatus.ALERTING -> 3
|
||||||
|
TrackerStatus.SUSPICIOUS -> 2
|
||||||
|
TrackerStatus.OBSERVED -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
private fun statusOf(t: TrackerEntity): TrackerStatus = when {
|
private fun statusOf(t: TrackerEntity): TrackerStatus = when {
|
||||||
t.approved -> TrackerStatus.SAFE_APPROVED
|
t.approved -> TrackerStatus.SAFE_APPROVED
|
||||||
t.baselineSafe -> TrackerStatus.SAFE_BASELINE
|
t.baselineSafe -> TrackerStatus.SAFE_BASELINE
|
||||||
@@ -163,12 +390,34 @@ private fun statusOf(t: TrackerEntity): TrackerStatus = when {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun statusColor(s: TrackerStatus): Color = when (s) {
|
||||||
|
TrackerStatus.ALERTING -> VigilRed
|
||||||
|
TrackerStatus.SUSPICIOUS -> VigilPeach
|
||||||
|
TrackerStatus.SAFE_APPROVED, TrackerStatus.SAFE_BASELINE -> VigilGreen
|
||||||
|
TrackerStatus.OBSERVED -> Color(0xFF89B4FA)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun statusIcon(s: TrackerStatus): ImageVector = when (s) {
|
||||||
|
TrackerStatus.ALERTING -> Icons.Filled.Warning
|
||||||
|
TrackerStatus.SUSPICIOUS -> Icons.Filled.GppMaybe
|
||||||
|
TrackerStatus.SAFE_APPROVED, TrackerStatus.SAFE_BASELINE -> Icons.Filled.Verified
|
||||||
|
TrackerStatus.OBSERVED -> Icons.Filled.Bluetooth
|
||||||
|
}
|
||||||
|
|
||||||
private fun statusLabel(s: TrackerStatus): String = when (s) {
|
private fun statusLabel(s: TrackerStatus): String = when (s) {
|
||||||
TrackerStatus.SAFE_APPROVED -> "Approved (yours)"
|
TrackerStatus.SAFE_APPROVED -> "Approved (yours)"
|
||||||
TrackerStatus.SAFE_BASELINE -> "Known (home)"
|
TrackerStatus.SAFE_BASELINE -> "Known (around home)"
|
||||||
TrackerStatus.ALERTING -> "⚠ Following you"
|
TrackerStatus.ALERTING -> "Following you"
|
||||||
TrackerStatus.SUSPICIOUS -> "Watching"
|
TrackerStatus.SUSPICIOUS -> "Watching"
|
||||||
TrackerStatus.OBSERVED -> "Seen once"
|
TrackerStatus.OBSERVED -> "Seen nearby"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun explanation(s: TrackerStatus): String = when (s) {
|
||||||
|
TrackerStatus.ALERTING -> "This tracker has been close to you across several distinct places over a sustained window — the signature of something travelling with you. If it isn't yours, treat it seriously."
|
||||||
|
TrackerStatus.SUSPICIOUS -> "Seen repeatedly and close, but not yet across enough places/time to confirm it's following you. VIGIL keeps watching."
|
||||||
|
TrackerStatus.OBSERVED -> "Seen nearby but with no co-movement pattern — most likely ambient."
|
||||||
|
TrackerStatus.SAFE_APPROVED -> "You marked this as yours, so VIGIL won't alert on it."
|
||||||
|
TrackerStatus.SAFE_BASELINE -> "Regularly around the places you live — learned as part of your normal environment, entirely on-device."
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ecosystemDisplay(name: String): String =
|
private fun ecosystemDisplay(name: String): String =
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
agp = "8.7.3"
|
agp = "8.7.3"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.0.21"
|
||||||
ksp = "2.0.21-1.0.28"
|
ksp = "2.0.21-1.0.28"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.15.0"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
activityCompose = "1.9.3"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2024.12.01"
|
composeBom = "2024.12.01"
|
||||||
material3 = "1.3.1"
|
material3 = "1.3.1"
|
||||||
playServicesLocation = "21.3.0"
|
playServicesLocation = "21.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user