Add OVERWATCH v0.1.0 — full detection engine + polish
Phase 1 (BLE), Phase 2 (WiFi BSSID/SSID), Phase 3 (DeFlock map proximity), Phase 4 (Waze live POLICE alerts), and Phase 5 polish all wired through one DetectionStore. Confidence engine scores 0-100; UI maps to 4-tier circle. Polish: - Stylized two-line app title: [DЯΣΛMMΛKΣЯ] // 0VΣЯW4TCH - Modal bottom sheet for source drill-down (tap circle) - Settings screen: per-source toggles, proximity sliders, theme select - SharedPreferences-backed Settings with StateFlow exposure - DetectionService respects per-source toggles at start time - Scanners read proximity overrides via supplier lambdas README documents all sources, architecture, build steps, permissions, and the legal disclaimer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
package org.soulstone.overwatch.ui
|
||||
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
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.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.soulstone.overwatch.fusion.DetectionEvent
|
||||
import org.soulstone.overwatch.fusion.DetectionSource
|
||||
import org.soulstone.overwatch.fusion.ThreatLevel
|
||||
import org.soulstone.overwatch.ui.theme.ThreatColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
running: Boolean,
|
||||
threat: ThreatLevel,
|
||||
score: Int,
|
||||
events: List<DetectionEvent>,
|
||||
onStartStop: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
canStart: Boolean,
|
||||
permissionMessage: String?
|
||||
) {
|
||||
var showSheet by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val sheetScope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "[DЯΣΛMMΛKΣЯ]",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
Text(
|
||||
text = " . //0VΣЯW4TCH",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onOpenSettings) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = "Settings",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(40.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ThreatCircle(level = threat, animating = running, onTap = { showSheet = true })
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "tap circle for source details",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
StatusText(running = running, threat = threat, score = score, events = events)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = onStartStop,
|
||||
enabled = canStart,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (running) ThreatColors.Red else ThreatColors.Green,
|
||||
contentColor = Color.White
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (running) "STOP" else "START",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
if (permissionMessage != null) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = permissionMessage,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showSheet = false },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
|
||||
Text(
|
||||
text = "Detection sources",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SourcesPanel(events = events)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreatCircle(level: ThreatLevel, animating: Boolean, onTap: () -> Unit) {
|
||||
val color = when (level) {
|
||||
ThreatLevel.GREEN -> ThreatColors.Green
|
||||
ThreatLevel.YELLOW -> ThreatColors.Yellow
|
||||
ThreatLevel.ORANGE -> ThreatColors.Orange
|
||||
ThreatLevel.RED -> ThreatColors.Red
|
||||
}
|
||||
val transition = rememberInfiniteTransition(label = "pulse")
|
||||
val pulse by transition.animateFloat(
|
||||
initialValue = if (animating) 0.6f else 1.0f,
|
||||
targetValue = 1.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1200),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "pulse"
|
||||
)
|
||||
val alpha = if (animating) pulse else 1.0f
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(220.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
color.copy(alpha = alpha),
|
||||
color.copy(alpha = alpha * 0.6f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.clickable(onClick = onTap),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = level.name,
|
||||
color = Color.White,
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusText(
|
||||
running: Boolean,
|
||||
threat: ThreatLevel,
|
||||
score: Int,
|
||||
events: List<DetectionEvent>
|
||||
) {
|
||||
val text = when {
|
||||
!running -> "Idle — press START to begin scanning"
|
||||
events.isEmpty() -> "All clear"
|
||||
threat == ThreatLevel.GREEN -> "Scanning… (${events.size} weak signals)"
|
||||
else -> {
|
||||
val top = events.first()
|
||||
"${top.label} • ${top.score}"
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
if (running) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Max score: $score",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourcesPanel(events: List<DetectionEvent>) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
DetectionSource.values().forEach { source ->
|
||||
val sourceEvents = events.filter { it.source == source }
|
||||
SourceRow(source = source, events = sourceEvents)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceRow(source: DetectionSource, events: List<DetectionEvent>) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = source.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
val maxScore = events.maxOfOrNull { it.score } ?: 0
|
||||
val statusColor = when {
|
||||
maxScore >= ThreatLevel.RED.minScore -> ThreatColors.Red
|
||||
maxScore >= ThreatLevel.ORANGE.minScore -> ThreatColors.Orange
|
||||
maxScore >= ThreatLevel.YELLOW.minScore -> ThreatColors.Yellow
|
||||
else -> ThreatColors.Green
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(statusColor)
|
||||
)
|
||||
}
|
||||
if (events.isEmpty()) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "no detections",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
events.take(3).forEach { e ->
|
||||
Text(
|
||||
text = "${e.score} • ${e.label} • ${e.matchedMethods}",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
if (events.size > 3) {
|
||||
Text(
|
||||
text = "+${events.size - 3} more",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package org.soulstone.overwatch.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.soulstone.overwatch.data.settings.Settings
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
settings: Settings,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val ble by settings.bleEnabled.collectAsState()
|
||||
val wifi by settings.wifiEnabled.collectAsState()
|
||||
val deflock by settings.deflockEnabled.collectAsState()
|
||||
val waze by settings.wazeEnabled.collectAsState()
|
||||
val deflockProx by settings.deflockProximityM.collectAsState()
|
||||
val wazeProx by settings.wazeProximityM.collectAsState()
|
||||
val theme by settings.themeMode.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
Text(
|
||||
text = "Settings",
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SectionLabel("Detection sources")
|
||||
SourceToggle("BLE • Bluetooth Low Energy", ble) { settings.setBleEnabled(it) }
|
||||
SourceToggle("WIFI • WiFi BSSID + SSID", wifi) { settings.setWifiEnabled(it) }
|
||||
SourceToggle("DEFLOCK • ALPR map (cdn.deflock.me)", deflock) { settings.setDeflockEnabled(it) }
|
||||
SourceToggle("WAZE • Live police reports", waze) { settings.setWazeEnabled(it) }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Source toggles take effect on next Start.",
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
SectionLabel("Proximity thresholds")
|
||||
SliderRow(
|
||||
label = "DeFlock alert distance",
|
||||
valueLabel = "${deflockProx} m",
|
||||
value = deflockProx.toFloat(),
|
||||
range = 50f..1600f,
|
||||
steps = 30,
|
||||
onChange = { settings.setDeflockProximityM(it.toInt()) }
|
||||
)
|
||||
SliderRow(
|
||||
label = "Waze alert distance",
|
||||
valueLabel = "${wazeProx} m",
|
||||
value = wazeProx.toFloat(),
|
||||
range = 100f..5000f,
|
||||
steps = 48,
|
||||
onChange = { settings.setWazeProximityM(it.toInt()) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionLabel("Appearance")
|
||||
ThemeRadio("System default", theme == Settings.ThemeMode.SYSTEM) {
|
||||
settings.setThemeMode(Settings.ThemeMode.SYSTEM)
|
||||
}
|
||||
ThemeRadio("Dark", theme == Settings.ThemeMode.DARK) {
|
||||
settings.setThemeMode(Settings.ThemeMode.DARK)
|
||||
}
|
||||
ThemeRadio("Light", theme == Settings.ThemeMode.LIGHT) {
|
||||
settings.setThemeMode(Settings.ThemeMode.LIGHT)
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionLabel(text: String) {
|
||||
Text(
|
||||
text = text.uppercase(),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceToggle(label: String, value: Boolean, onChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
Switch(checked = value, onCheckedChange = onChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SliderRow(
|
||||
label: String,
|
||||
valueLabel: String,
|
||||
value: Float,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
steps: Int,
|
||||
onChange: (Float) -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
Text(
|
||||
text = valueLabel,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 13.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onChange,
|
||||
valueRange = range,
|
||||
steps = steps
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeRadio(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(selected = selected, onClick = onClick)
|
||||
Text(
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.soulstone.overwatch.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.soulstone.overwatch.data.settings.Settings
|
||||
|
||||
private val DarkColors = darkColorScheme(
|
||||
primary = Color(0xFF1FAA59),
|
||||
onPrimary = Color.White,
|
||||
background = Color(0xFF0B0E12),
|
||||
onBackground = Color(0xFFF4F6FA),
|
||||
surface = Color(0xFF161A21),
|
||||
onSurface = Color(0xFFF4F6FA),
|
||||
surfaceVariant = Color(0xFF1E232C),
|
||||
onSurfaceVariant = Color(0xFF9AA3B2)
|
||||
)
|
||||
|
||||
private val LightColors = lightColorScheme(
|
||||
primary = Color(0xFF1FAA59),
|
||||
onPrimary = Color.White,
|
||||
background = Color(0xFFF4F6FA),
|
||||
onBackground = Color(0xFF0B0E12),
|
||||
surface = Color.White,
|
||||
onSurface = Color(0xFF0B0E12)
|
||||
)
|
||||
|
||||
object ThreatColors {
|
||||
val Green = Color(0xFF1FAA59)
|
||||
val Yellow = Color(0xFFF4C20D)
|
||||
val Orange = Color(0xFFF26B0F)
|
||||
val Red = Color(0xFFD7263D)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OverwatchTheme(
|
||||
mode: Settings.ThemeMode = Settings.ThemeMode.DARK,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val dark = when (mode) {
|
||||
Settings.ThemeMode.DARK -> true
|
||||
Settings.ThemeMode.LIGHT -> false
|
||||
Settings.ThemeMode.SYSTEM -> isSystemInDarkTheme()
|
||||
}
|
||||
MaterialTheme(
|
||||
colorScheme = if (dark) DarkColors else LightColors,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user