rebranding work

This commit is contained in:
2026-05-25 21:53:01 -04:00
parent 89822dedb8
commit c01c1a5636
52 changed files with 3090 additions and 70 deletions

View File

@@ -0,0 +1,61 @@
package com.shieldai.android.data.repository
import android.content.Context
import com.shieldai.android.data.local.CacheManager
import com.shieldai.android.data.model.Property
import com.shieldai.android.data.remote.ApiResult
import com.shieldai.android.data.remote.ErrorHandler
import com.shieldai.android.data.remote.TRPCApiService
import com.shieldai.android.data.remote.TRPCRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
class HomeTitleRepository(
private val api: TRPCApiService,
private val context: Context,
) {
private val _properties = MutableStateFlow<List<Property>>(emptyList())
suspend fun getProperties(forceRefresh: Boolean = false): ApiResult<List<Property>> {
if (!forceRefresh) {
val cached: List<Property>? = CacheManager.load(context, "properties")
if (cached != null) {
_properties.value = cached
return ApiResult.Success(cached)
}
}
return ErrorHandler.executeWithRetry {
val response = api.propertyList(TRPCRequest.body(buildJsonObject {}))
val properties = response.result.data
CacheManager.save(context, "properties", properties)
_properties.value = properties
properties
}
}
suspend fun addProperty(address: String, type: String = "residential"): ApiResult<Property> {
return ErrorHandler.executeWithRetry {
val body = buildJsonObject {
put("address", address)
put("type", type)
}
val response = api.propertyAdd(TRPCRequest.body(body))
val property = response.result.data
refreshCache()
property
}
}
fun observeProperties(): Flow<List<Property>> = _properties
private suspend fun refreshCache() {
ErrorHandler.executeWithRetry {
val response = api.propertyList(TRPCRequest.body(buildJsonObject {}))
val properties = response.result.data
CacheManager.save(context, "properties", properties)
_properties.value = properties
}
}
}

View File

@@ -0,0 +1,81 @@
package com.shieldai.android.data.repository
import android.content.Context
import com.shieldai.android.data.local.CacheManager
import com.shieldai.android.data.model.BrokerListing
import com.shieldai.android.data.model.RemovalRequest
import com.shieldai.android.data.remote.ApiResult
import com.shieldai.android.data.remote.ErrorHandler
import com.shieldai.android.data.remote.TRPCApiService
import com.shieldai.android.data.remote.TRPCRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
class RemoveBrokersRepository(
private val api: TRPCApiService,
private val context: Context,
) {
private val _listings = MutableStateFlow<List<BrokerListing>>(emptyList())
private val _removalRequests = MutableStateFlow<List<RemovalRequest>>(emptyList())
suspend fun getListings(forceRefresh: Boolean = false): ApiResult<List<BrokerListing>> {
if (!forceRefresh) {
val cached: List<BrokerListing>? = CacheManager.load(context, "broker_listings")
if (cached != null) {
_listings.value = cached
return ApiResult.Success(cached)
}
}
return ErrorHandler.executeWithRetry {
val response = api.brokerListListings(TRPCRequest.body(buildJsonObject {}))
val listings = response.result.data
CacheManager.save(context, "broker_listings", listings)
_listings.value = listings
listings
}
}
suspend fun getRemovalRequests(forceRefresh: Boolean = false): ApiResult<List<RemovalRequest>> {
if (!forceRefresh) {
val cached: List<RemovalRequest>? = CacheManager.load(context, "removal_requests")
if (cached != null) {
_removalRequests.value = cached
return ApiResult.Success(cached)
}
}
return ErrorHandler.executeWithRetry {
val response = api.removalList(TRPCRequest.body(buildJsonObject {}))
val requests = response.result.data
CacheManager.save(context, "removal_requests", requests)
_removalRequests.value = requests
requests
}
}
suspend fun createRemovalRequest(listingId: String, notes: String? = null): ApiResult<RemovalRequest> {
return ErrorHandler.executeWithRetry {
val body = buildJsonObject {
put("listingId", listingId)
notes?.let { put("notes", it) }
}
val response = api.removalCreate(TRPCRequest.body(body))
val request = response.result.data
refreshRemovalsCache()
request
}
}
fun observeListings(): Flow<List<BrokerListing>> = _listings
fun observeRemovalRequests(): Flow<List<RemovalRequest>> = _removalRequests
private suspend fun refreshRemovalsCache() {
ErrorHandler.executeWithRetry {
val response = api.removalList(TRPCRequest.body(buildJsonObject {}))
val requests = response.result.data
CacheManager.save(context, "removal_requests", requests)
_removalRequests.value = requests
}
}
}

View File

@@ -0,0 +1,86 @@
package com.shieldai.android.data.repository
import android.content.Context
import com.shieldai.android.data.local.CacheManager
import com.shieldai.android.data.model.SpamRule
import com.shieldai.android.data.remote.ApiResult
import com.shieldai.android.data.remote.ErrorHandler
import com.shieldai.android.data.remote.TRPCApiService
import com.shieldai.android.data.remote.TRPCRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
class SpamShieldRepository(
private val api: TRPCApiService,
private val context: Context,
) {
private val _rules = MutableStateFlow<List<SpamRule>>(emptyList())
data class SpamStats(
val totalBlocked: Int = 0,
val totalFlagged: Int = 0,
val activeRules: Int = 0
)
suspend fun getRules(forceRefresh: Boolean = false): ApiResult<List<SpamRule>> {
if (!forceRefresh) {
val cached: List<SpamRule>? = CacheManager.load(context, "spam_rules")
if (cached != null) {
_rules.value = cached
return ApiResult.Success(cached)
}
}
return ErrorHandler.executeWithRetry {
val response = api.spamListRules(TRPCRequest.body(buildJsonObject {}))
val rules = response.result.data
CacheManager.save(context, "spam_rules", rules)
_rules.value = rules
rules
}
}
suspend fun createRule(pattern: String, action: String, description: String? = null): ApiResult<SpamRule> {
return ErrorHandler.executeWithRetry {
val body = buildJsonObject {
put("pattern", pattern)
put("action", action)
description?.let { put("description", it) }
}
val response = api.spamCreateRule(TRPCRequest.body(body))
val rule = response.result.data
refreshCache()
rule
}
}
suspend fun toggleRule(id: String, enabled: Boolean): ApiResult<Unit> {
return ErrorHandler.executeWithRetry {
_rules.value = _rules.value.map {
if (it.id == id) it.copy(enabled = enabled) else it
}
refreshCache()
}
}
fun getStats(): SpamStats {
val rules = _rules.value
return SpamStats(
totalBlocked = rules.count { it.action == "block" && it.enabled },
totalFlagged = rules.count { it.action == "flag" && it.enabled },
activeRules = rules.count { it.enabled }
)
}
fun observeRules(): Flow<List<SpamRule>> = _rules
private suspend fun refreshCache() {
ErrorHandler.executeWithRetry {
val response = api.spamListRules(TRPCRequest.body(buildJsonObject {}))
val rules = response.result.data
CacheManager.save(context, "spam_rules", rules)
_rules.value = rules
}
}
}

View File

@@ -3,6 +3,9 @@ package com.shieldai.android.di
import android.content.Context
import com.shieldai.android.data.repository.AlertRepository
import com.shieldai.android.data.repository.DarkWatchRepository
import com.shieldai.android.data.repository.HomeTitleRepository
import com.shieldai.android.data.repository.RemoveBrokersRepository
import com.shieldai.android.data.repository.SpamShieldRepository
import com.shieldai.android.data.repository.SubscriptionRepository
import com.shieldai.android.data.repository.UserRepository
import com.shieldai.android.data.repository.VoicePrintRepository
@@ -13,6 +16,9 @@ object RepositoryModule {
private var voicePrintRepository: VoicePrintRepository? = null
private var alertRepository: AlertRepository? = null
private var subscriptionRepository: SubscriptionRepository? = null
private var spamShieldRepository: SpamShieldRepository? = null
private var homeTitleRepository: HomeTitleRepository? = null
private var removeBrokersRepository: RemoveBrokersRepository? = null
fun provideUserRepository(context: Context): UserRepository {
return userRepository ?: synchronized(this) {
@@ -58,4 +64,31 @@ object RepositoryModule {
).also { subscriptionRepository = it }
}
}
fun provideSpamShieldRepository(context: Context): SpamShieldRepository {
return spamShieldRepository ?: synchronized(this) {
SpamShieldRepository(
api = NetworkModule.provideApiService(context),
context = context,
).also { spamShieldRepository = it }
}
}
fun provideHomeTitleRepository(context: Context): HomeTitleRepository {
return homeTitleRepository ?: synchronized(this) {
HomeTitleRepository(
api = NetworkModule.provideApiService(context),
context = context,
).also { homeTitleRepository = it }
}
}
fun provideRemoveBrokersRepository(context: Context): RemoveBrokersRepository {
return removeBrokersRepository ?: synchronized(this) {
RemoveBrokersRepository(
api = NetworkModule.provideApiService(context),
context = context,
).also { removeBrokersRepository = it }
}
}
}

View File

@@ -0,0 +1,107 @@
package com.shieldai.android.ui.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import com.shieldai.android.ui.theme.Error
import com.shieldai.android.ui.theme.Success
import com.shieldai.android.ui.theme.TextPrimaryLight
import com.shieldai.android.ui.theme.Warning
@Composable
fun ThreatGauge(
score: Int,
modifier: Modifier = Modifier,
size: Int = 160
) {
val (startColor, endColor) = when {
score <= 30 -> Success to Success.copy(alpha = 0.4f)
score <= 60 -> Warning to Warning.copy(alpha = 0.4f)
else -> Error to Error.copy(alpha = 0.4f)
}
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Canvas(
modifier = Modifier.size(size.dp)
) {
val center = Offset(size / 2f, size / 2f)
val radius = (size / 2f - 16f) * dpiScale
val strokeWidth = 16f * dpiScale
drawArc(
color = Color(0xFFE2E8F0).copy(alpha = 0.3f),
startAngle = -135f,
sweepAngle = 270f,
useCenter = false,
topLeft = Offset(
center.x - radius,
center.y - radius
),
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2),
style = Stroke(width = strokeWidth)
)
val sweepAngle = (score / 100f) * 270f
if (sweepAngle > 0) {
val gradient = Brush.linearGradient(
colors = listOf(startColor, endColor),
start = Offset(center.x - radius, center.y),
end = Offset(center.x + radius, center.y)
)
drawArc(
brush = gradient,
startAngle = -135f,
sweepAngle = sweepAngle,
useCenter = false,
topLeft = Offset(
center.x - radius,
center.y - radius
),
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2),
style = Stroke(width = strokeWidth, cap = androidx.compose.ui.graphics.Paint.Cap.Round)
)
}
}
Text(
text = "$score",
style = MaterialTheme.typography.displayMedium,
color = when {
score <= 30 -> Success
score <= 60 -> Warning
else -> Error
},
modifier = Modifier.padding(top = 8.dp)
)
Text(
text = when {
score <= 30 -> "Low Risk"
score <= 60 -> "Medium Risk"
else -> "High Risk"
},
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
)
}
}
private val Float.dpiScale: Float
get() = 1f

View File

@@ -0,0 +1,281 @@
package com.shieldai.android.ui.screens.dashboard
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
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.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.shieldai.android.data.model.Alert
import com.shieldai.android.ui.components.BadgeVariant
import com.shieldai.android.ui.components.ShieldBadge
import com.shieldai.android.ui.components.ShieldButton
import com.shieldai.android.ui.components.ShieldButtonVariant
import com.shieldai.android.ui.components.ShieldCard
import com.shieldai.android.ui.components.ShieldEmptyState
import com.shieldai.android.ui.viewmodel.AlertDetailViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlertDetailScreen(
alertId: String,
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: AlertDetailViewModel = viewModel(factory = AlertDetailViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.alert == null) {
viewModel.loadAlert(alertId)
}
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = {
Text(
text = uiState.alert?.title ?: "Alert Details",
fontWeight = FontWeight.SemiBold
)
},
navigationIcon = {
TextButton(onClick = onBack) {
Text("Back")
}
},
scrollBehavior = scrollBehavior
)
}
) { paddingValues ->
when {
uiState.isLoading -> {
Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
uiState.alert == null -> {
ShieldEmptyState(
title = "Alert not found",
description = "The requested alert could not be loaded",
actionButton = {
TextButton(onClick = onBack) {
Text("Go Back")
}
},
modifier = Modifier.padding(paddingValues)
)
}
else -> {
AlertDetailContent(
uiState = uiState,
onMarkResolved = { viewModel.markResolved() },
onMarkFalsePositive = { viewModel.markFalsePositive() },
modifier = Modifier.padding(paddingValues)
)
}
}
}
}
@Composable
private fun AlertDetailContent(
uiState: AlertDetailViewModel.AlertDetailUiState,
onMarkResolved: () -> Unit,
onMarkFalsePositive: () -> Unit,
modifier: Modifier = Modifier
) {
val alert = uiState.alert!!
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
AlertDetailHeader(alert)
}
item {
AlertDetailInfo(alert)
}
if (uiState.correlatedAlerts.isNotEmpty()) {
item {
Text(
text = "Correlated Alerts (${uiState.correlatedAlerts.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.correlatedAlerts) { correlated ->
CorrelatedAlertItem(correlated)
Spacer(modifier = Modifier.height(8.dp))
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
ShieldButton(
text = "Mark Resolved",
onClick = onMarkResolved,
variant = ShieldButtonVariant.Primary,
modifier = Modifier.weight(1f),
loading = uiState.isResolving
)
ShieldButton(
text = "False Positive",
onClick = onMarkFalsePositive,
variant = ShieldButtonVariant.Secondary,
modifier = Modifier.weight(1f)
)
}
}
}
}
@Composable
private fun AlertDetailHeader(alert: Alert) {
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val variant = when (alert.severity.lowercase()) {
"critical" -> BadgeVariant.Error
"high" -> BadgeVariant.Warning
"medium" -> BadgeVariant.Info
else -> BadgeVariant.Default
}
ShieldBadge(
text = "${alert.severity} severity",
variant = variant
)
if (!alert.read) {
ShieldBadge(
text = "Unread",
variant = BadgeVariant.Info
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = alert.title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = alert.message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun AlertDetailInfo(alert: Alert) {
ShieldCard {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Details",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)
InfoRow(label = "Type", value = alert.type)
InfoRow(label = "Severity", value = alert.severity)
InfoRow(label = "Status", value = if (alert.read) "Read" else "Unread")
alert.date?.let {
InfoRow(label = "Date", value = it)
}
alert.createdAt?.let {
InfoRow(label = "Created", value = it)
}
}
}
}
@Composable
private fun InfoRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
}
}
@Composable
private fun CorrelatedAlertItem(alert: Alert) {
ShieldCard(
modifier = Modifier.fillMaxWidth()
) {
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = alert.title,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
)
AlertSeverityBadge(severity = alert.severity)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = alert.message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2
)
}
}
}

View File

@@ -0,0 +1,325 @@
package com.shieldai.android.ui.screens.dashboard
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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PullToRefreshBox
import androidx.compose.material3.Text
import androidx.compose.material3.pullToRefresh
import androidx.compose.material3.pullToRefreshDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.shieldai.android.R
import com.shieldai.android.data.model.Alert
import com.shieldai.android.ui.components.BadgeVariant
import com.shieldai.android.ui.components.ShieldBadge
import com.shieldai.android.ui.components.ShieldCard
import com.shieldai.android.ui.components.ShieldEmptyState
import com.shieldai.android.ui.components.ShieldSkeletonCard
import com.shieldai.android.ui.components.ThreatGauge
import com.shieldai.android.viewmodel.DashboardViewModel
import com.shieldai.android.viewmodel.DashboardViewModel as DashboardVM
data class ServiceSummary(
val name: String,
val count: Int,
val icon: ImageVector,
val route: String
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DashboardScreen(
onNavigateToAlert: (String) -> Unit = {},
onNavigateToService: (String) -> Unit = {},
modifier: Modifier = Modifier,
viewModel: DashboardViewModel = viewModel(factory = DashboardVM.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var isRefreshing by remember { mutableStateOf(false) }
var isRefreshingInternal by remember { mutableStateOf(false) }
if (isRefreshing) {
viewModel.refresh()
isRefreshing = false
}
PullToRefreshBox(
isRefreshing = isRefreshingInternal || uiState.isLoading,
onRefresh = {
isRefreshingInternal = false
viewModel.refresh()
},
modifier = modifier,
indicator = {
androidx.compose.material3.pullToRefreshDefaults.indicator(
parent,
transformer,
isRefreshingInternal || uiState.isLoading
)
}
) {
when {
uiState.isLoading && uiState.recentAlerts.isEmpty() -> {
DashboardLoadingState()
}
uiState.recentAlerts.isEmpty() && uiState.threatScore == 0 -> {
if (uiState.error != null) {
ShieldEmptyState(
title = "Failed to load",
description = uiState.error ?: "Unknown error",
actionButton = {
androidx.compose.material3.TextButton(onClick = { viewModel.refresh() }) {
Text("Retry")
}
}
)
} else {
ShieldEmptyState(
title = "No data",
description = "No dashboard data available"
)
}
}
else -> {
DashboardContent(
uiState = uiState,
onNavigateToAlert = onNavigateToAlert,
onNavigateToService = onNavigateToService
)
}
}
}
}
@Composable
private fun DashboardLoadingState() {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Box(
modifier = Modifier.fillMaxWidth().padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
item { ShieldSkeletonCard() }
item { ShieldSkeletonCard() }
item { ShieldSkeletonCard() }
}
}
@Composable
private fun DashboardContent(
uiState: DashboardViewModel.DashboardUiState,
onNavigateToAlert: (String) -> Unit,
onNavigateToService: (String) -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(
horizontal = 16.dp,
vertical = 16.dp
),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
DashboardHeader(uiState)
}
item {
ServiceSummaryRow(
uiState = uiState,
onNavigateToService = onNavigateToService
)
}
if (uiState.recentAlerts.isNotEmpty()) {
item {
Text(
text = "Recent Alerts",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.recentAlerts) { alert ->
AlertCard(alert, onNavigateToAlert)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun DashboardHeader(uiState: DashboardViewModel.DashboardUiState) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
) {
Text(
text = "Threat Overview",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 16.dp)
)
ThreatGauge(score = uiState.threatScore)
if (uiState.unreadCount > 0) {
ShieldBadge(
text = "${uiState.unreadCount} unread alert${if (uiState.unreadCount > 1) "s" else ""}",
variant = BadgeVariant.Warning,
modifier = Modifier.padding(top = 12.dp)
)
}
}
}
@Composable
private fun ServiceSummaryRow(
uiState: DashboardViewModel.DashboardUiState,
onNavigateToService: (String) -> Unit
) {
val services = listOf(
ServiceSummary("DarkWatch", uiState.watchlistCount, ImageVector.vectorResource(R.drawable.ic_services), "darkwatch"),
ServiceSummary("VoicePrint", uiState.enrollmentCount, ImageVector.vectorResource(R.drawable.ic_services), "voiceprint"),
ServiceSummary("SpamShield", uiState.spamRulesCount, ImageVector.vectorResource(R.drawable.ic_services), "spamshield"),
ServiceSummary("HomeTitle", uiState.propertiesCount, ImageVector.vectorResource(R.drawable.ic_services), "hometitle"),
ServiceSummary("RemoveBrokers", uiState.removalsCount, ImageVector.vectorResource(R.drawable.ic_services), "removebrokers")
)
Text(
text = "Services",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(services) { service ->
ServiceCard(
service = service,
onClick = { onNavigateToService(service.route) }
)
}
}
}
@Composable
private fun ServiceCard(
service: ServiceSummary,
onClick: () -> Unit
) {
ShieldCard(
onClick = onClick,
modifier = Modifier.width(130.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(8.dp)
) {
Icon(
imageVector = service.icon,
contentDescription = service.name,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = service.name,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium
)
Text(
text = "${service.count}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
}
@Composable
private fun AlertCard(
alert: Alert,
onClick: (String) -> Unit
) {
ShieldCard(
onClick = { onClick(alert.id) },
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = alert.title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = alert.message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2
)
}
AlertSeverityBadge(severity = alert.severity)
}
}
}
@Composable
private fun AlertSeverityBadge(severity: String) {
val variant = when (severity.lowercase()) {
"critical" -> BadgeVariant.Error
"high" -> BadgeVariant.Warning
"medium" -> BadgeVariant.Info
else -> BadgeVariant.Default
}
ShieldBadge(
text = severity,
variant = variant
)
}

View File

@@ -0,0 +1,332 @@
package com.shieldai.android.ui.screens.services
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.shieldai.android.R
import com.shieldai.android.ui.components.ShieldBadge
import com.shieldai.android.ui.components.ShieldButton
import com.shieldai.android.ui.components.ShieldButtonVariant
import com.shieldai.android.ui.components.ShieldCard
import com.shieldai.android.ui.components.ShieldEmptyState
import com.shieldai.android.ui.components.ShieldTextField
import com.shieldai.android.viewmodel.DarkWatchViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DarkWatchScreen(
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: DarkWatchViewModel = viewModel(factory = DarkWatchViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var showAddSheet by remember { mutableStateOf(false) }
var newType by remember { mutableStateOf("email") }
var newValue by remember { mutableStateOf("") }
var newLabel by remember { mutableStateOf("") }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("DarkWatch", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
TextButton(onClick = onBack) { Text("Back") }
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
if (!showAddSheet) {
FloatingActionButton(onClick = { showAddSheet = true }) {
Icon(
painter = painterResource(R.drawable.ic_dashboard),
contentDescription = "Add to watchlist"
)
}
}
}
) { paddingValues ->
when {
uiState.isLoading && uiState.watchlist.isEmpty() -> {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
uiState.watchlist.isEmpty() && uiState.exposures.isEmpty() -> {
ShieldEmptyState(
title = "No watchlist items",
description = "Add people to monitor for data exposures",
actionButton = {
ShieldButton(
text = "Add to Watchlist",
onClick = { showAddSheet = true },
variant = ShieldButtonVariant.Primary
)
},
modifier = Modifier.padding(paddingValues)
)
}
else -> {
DarkWatchContent(
uiState = uiState,
modifier = Modifier.padding(paddingValues)
)
}
}
if (showAddSheet) {
AddWatchlistSheet(
onDismiss = {
showAddSheet = false
newValue = ""
newLabel = ""
},
onAdd = {
viewModel.addWatchlistItem(newType, newValue, newLabel.ifBlank { null })
showAddSheet = false
newValue = ""
newLabel = ""
},
type = newType,
onTypeChange = { newType = it },
value = newValue,
onValueChange = { newValue = it },
label = newLabel,
onLabelChange = { newLabel = it },
isLoading = uiState.isAdding
)
}
}
}
@Composable
private fun DarkWatchContent(
uiState: DarkWatchViewModel.DarkWatchUiState,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (uiState.watchlist.isNotEmpty()) {
item {
Text(
text = "Watchlist (${uiState.watchlist.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.watchlist) { item ->
WatchlistItemCard(item)
Spacer(modifier = Modifier.height(8.dp))
}
}
if (uiState.exposures.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Exposures (${uiState.exposures.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.exposures) { exposure ->
ExposureCard(exposure)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun WatchlistItemCard(item: com.shieldai.android.data.model.WatchlistItem) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.value,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
if (item.label != null) {
Text(
text = item.label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = item.type,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
ShieldBadge(
text = item.status,
variant = if (item.status == "active") com.shieldai.android.ui.components.BadgeVariant.Success
else com.shieldai.android.ui.components.BadgeVariant.Default
)
}
}
}
@Composable
private fun ExposureCard(exposure: com.shieldai.android.data.model.Exposure) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = exposure.source,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
)
ShieldBadge(
text = exposure.severity,
variant = when (exposure.severity.lowercase()) {
"critical" -> com.shieldai.android.ui.components.BadgeVariant.Error
"high" -> com.shieldai.android.ui.components.BadgeVariant.Warning
else -> com.shieldai.android.ui.components.BadgeVariant.Info
}
)
}
exposure.details?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AddWatchlistSheet(
onDismiss: () -> Unit,
onAdd: () -> Unit,
type: String,
onTypeChange: (String) -> Unit,
value: String,
onValueChange: (String) -> Unit,
label: String,
onLabelChange: (String) -> Unit,
isLoading: Boolean
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) {
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Add to Watchlist",
style = MaterialTheme.typography.titleLarge
)
androidx.compose.material3.ExposedDropdownMenuBox(
expanded = false,
onExpandedChange = {}
) {
ShieldTextField(
value = type,
onValueChange = onTypeChange,
label = "Type",
modifier = Modifier.fillMaxWidth()
)
}
ShieldTextField(
value = value,
onValueChange = onValueChange,
label = "Value (email, name, etc.)",
modifier = Modifier.fillMaxWidth()
)
ShieldTextField(
value = label,
onValueChange = onLabelChange,
label = "Label (optional)",
modifier = Modifier.fillMaxWidth()
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
ShieldButton(
text = "Cancel",
onClick = onDismiss,
variant = ShieldButtonVariant.Secondary,
modifier = Modifier.weight(1f)
)
ShieldButton(
text = "Add",
onClick = onAdd,
variant = ShieldButtonVariant.Primary,
modifier = Modifier.weight(1f),
enabled = value.isNotBlank(),
loading = isLoading
)
}
}
}
}

View File

@@ -0,0 +1,256 @@
package com.shieldai.android.ui.screens.services
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.shieldai.android.R
import com.shieldai.android.ui.components.BadgeVariant
import com.shieldai.android.ui.components.ShieldBadge
import com.shieldai.android.ui.components.ShieldButton
import com.shieldai.android.ui.components.ShieldButtonVariant
import com.shieldai.android.ui.components.ShieldCard
import com.shieldai.android.ui.components.ShieldEmptyState
import com.shieldai.android.ui.components.ShieldTextField
import com.shieldai.android.viewmodel.VoicePrintViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VoicePrintScreen(
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: VoicePrintViewModel = viewModel(factory = VoicePrintViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var showEnrollSheet by remember { mutableStateOf(false) }
var enrollmentName by remember { mutableStateOf("") }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("VoicePrint", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
TextButton(onClick = onBack) { Text("Back") }
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
if (!showEnrollSheet) {
FloatingActionButton(onClick = { showEnrollSheet = true }) {
Icon(
painter = painterResource(R.drawable.ic_dashboard),
contentDescription = "New enrollment"
)
}
}
}
) { paddingValues ->
when {
uiState.isLoading && uiState.enrollments.isEmpty() -> {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
uiState.enrollments.isEmpty() -> {
ShieldEmptyState(
title = "No enrollments",
description = "Enroll voice profiles to detect impersonation",
actionButton = {
ShieldButton(
text = "New Enrollment",
onClick = { showEnrollSheet = true },
variant = ShieldButtonVariant.Primary
)
},
modifier = Modifier.padding(paddingValues)
)
}
else -> {
VoicePrintContent(
uiState = uiState,
modifier = Modifier.padding(paddingValues)
)
}
}
if (showEnrollSheet) {
EnrollSheet(
onDismiss = {
showEnrollSheet = false
enrollmentName = ""
},
onEnroll = {
viewModel.createEnrollment(enrollmentName)
showEnrollSheet = false
enrollmentName = ""
},
name = enrollmentName,
onNameChange = { enrollmentName = it },
isLoading = uiState.isEnrolling
)
}
}
}
@Composable
private fun VoicePrintContent(
uiState: VoicePrintViewModel.VoicePrintUiState,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(
text = "Enrollments (${uiState.enrollments.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.enrollments) { enrollment ->
EnrollmentCard(enrollment)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
private fun EnrollmentCard(enrollment: com.shieldai.android.data.model.VoiceEnrollment) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = enrollment.name,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Text(
text = "${enrollment.sampleCount} samples",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
ShieldBadge(
text = enrollment.status,
variant = when (enrollment.status.lowercase()) {
"active" -> BadgeVariant.Success
"pending" -> BadgeVariant.Warning
else -> BadgeVariant.Default
}
)
}
enrollment.createdAt?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Created: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EnrollSheet(
onDismiss: () -> Unit,
onEnroll: () -> Unit,
name: String,
onNameChange: (String) -> Unit,
isLoading: Boolean
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) {
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "New Voice Enrollment",
style = MaterialTheme.typography.titleLarge
)
Text(
text = "Enter a name for this voice profile. You will be able to record samples afterwards.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
ShieldTextField(
value = name,
onValueChange = onNameChange,
label = "Profile name",
modifier = Modifier.fillMaxWidth()
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
ShieldButton(
text = "Cancel",
onClick = onDismiss,
variant = ShieldButtonVariant.Secondary,
modifier = Modifier.weight(1f)
)
ShieldButton(
text = "Enroll",
onClick = onEnroll,
variant = ShieldButtonVariant.Primary,
modifier = Modifier.weight(1f),
enabled = name.isNotBlank(),
loading = isLoading
)
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.Alert
import com.shieldai.android.data.repository.AlertRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class AlertDetailUiState(
val alert: Alert? = null,
val correlatedAlerts: List<Alert> = emptyList(),
val isLoading: Boolean = true,
val isResolving: Boolean = false,
val error: String? = null
)
class AlertDetailViewModel : ViewModel() {
private val _uiState = MutableStateFlow(AlertDetailUiState())
val uiState: StateFlow<AlertDetailUiState> = _uiState.asStateFlow()
private val alertRepo: AlertRepository by lazy {
RepositoryModule.provideAlertRepository(ShieldAIApp.instance)
}
fun loadAlert(alertId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
try {
val result = alertRepo.getAlerts()
if (result is com.shieldai.android.data.remote.ApiResult.Success) {
val alert = result.data.find { it.id == alertId }
val correlated = alert?.let {
result.data.filter { a ->
a.id != alertId && a.type == it.type
}
} ?: emptyList()
_uiState.value = _uiState.value.copy(
isLoading = false,
alert = alert,
correlatedAlerts = correlated
)
alert?.let { markAlertRead(it.id) }
} else {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Failed to load alert"
)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load alert"
)
}
}
}
fun markResolved() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isResolving = true)
val alert = _uiState.value.alert
alert?.let { markAlertRead(it.id) }
_uiState.value = _uiState.value.copy(isResolving = false)
}
}
fun markFalsePositive() {
viewModelScope.launch {
val alert = _uiState.value.alert
alert?.let { markAlertRead(it.id) }
}
}
private fun markAlertRead(alertId: String) {
alertRepo.markRead(alertId)
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AlertDetailViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,100 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.Exposure
import com.shieldai.android.data.model.WatchlistItem
import com.shieldai.android.data.repository.DarkWatchRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class DarkWatchUiState(
val watchlist: List<WatchlistItem> = emptyList(),
val exposures: List<Exposure> = emptyList(),
val isLoading: Boolean = true,
val isAdding: Boolean = false,
val error: String? = null
)
class DarkWatchViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DarkWatchUiState())
val uiState: StateFlow<DarkWatchUiState> = _uiState.asStateFlow()
private val repo: DarkWatchRepository by lazy {
RepositoryModule.provideDarkWatchRepository(ShieldAIApp.instance)
}
init {
loadData()
}
fun refresh() {
loadData(forceRefresh = true)
}
private fun loadData(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val watchlistResult = repo.getWatchlist(forceRefresh)
val exposuresResult = repo.getExposures(forceRefresh)
val watchlist = if (watchlistResult is com.shieldai.android.data.remote.ApiResult.Success) {
watchlistResult.data
} else emptyList()
val exposures = if (exposuresResult is com.shieldai.android.data.remote.ApiResult.Success) {
exposuresResult.data
} else emptyList()
_uiState.value = _uiState.value.copy(
isLoading = false,
watchlist = watchlist,
exposures = exposures
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load data"
)
}
}
}
fun addWatchlistItem(type: String, value: String, label: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isAdding = true, error = null)
val result = repo.addWatchlistItem(type, value, label)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isAdding = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isAdding = false)
loadData(forceRefresh = true)
}
}
}
fun removeWatchlistItem(id: String) {
viewModelScope.launch {
repo.removeWatchlistItem(id)
loadData(forceRefresh = true)
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return DarkWatchViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,154 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.Alert
import com.shieldai.android.data.repository.AlertRepository
import com.shieldai.android.data.repository.DarkWatchRepository
import com.shieldai.android.data.repository.HomeTitleRepository
import com.shieldai.android.data.repository.RemoveBrokersRepository
import com.shieldai.android.data.repository.SpamShieldRepository
import com.shieldai.android.data.repository.VoicePrintRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class DashboardUiState(
val isLoading: Boolean = false,
val threatScore: Int = 0,
val recentAlerts: List<Alert> = emptyList(),
val unreadCount: Int = 0,
val watchlistCount: Int = 0,
val enrollmentCount: Int = 0,
val spamRulesCount: Int = 0,
val propertiesCount: Int = 0,
val removalsCount: Int = 0,
val error: String? = null
)
class DashboardViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
private val alertRepo: AlertRepository by lazy {
RepositoryModule.provideAlertRepository(ShieldAIApp.instance)
}
private val darkWatchRepo: DarkWatchRepository by lazy {
RepositoryModule.provideDarkWatchRepository(ShieldAIApp.instance)
}
private val voicePrintRepo: VoicePrintRepository by lazy {
RepositoryModule.provideVoicePrintRepository(ShieldAIApp.instance)
}
private val spamShieldRepo: SpamShieldRepository by lazy {
RepositoryModule.provideSpamShieldRepository(ShieldAIApp.instance)
}
private val homeTitleRepo: HomeTitleRepository by lazy {
RepositoryModule.provideHomeTitleRepository(ShieldAIApp.instance)
}
private val removeBrokersRepo: RemoveBrokersRepository by lazy {
RepositoryModule.provideRemoveBrokersRepository(ShieldAIApp.instance)
}
init {
loadDashboardData()
}
fun refresh() {
loadDashboardData(true)
}
private fun loadDashboardData(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val alertsResult = alertRepo.getAlerts()
val watchlistResult = darkWatchRepo.getWatchlist(forceRefresh)
val enrollmentsResult = voicePrintRepo.getEnrollments()
val rulesResult = spamShieldRepo.getRules()
val propertiesResult = homeTitleRepo.getProperties()
val removalsResult = removeBrokersRepo.getRemovalRequests()
val alerts = when (alertsResult) {
is com.shieldai.android.data.remote.ApiResult.Success -> alertsResult.data
else -> emptyList()
}
val watchlist = when (watchlistResult) {
is com.shieldai.android.data.remote.ApiResult.Success -> watchlistResult.data
else -> emptyList()
}
val enrollments = when (enrollmentsResult) {
is com.shieldai.android.data.remote.ApiResult.Success -> enrollmentsResult.data
else -> emptyList()
}
val rules = when (rulesResult) {
is com.shieldai.android.data.remote.ApiResult.Success -> rulesResult.data
else -> emptyList()
}
val properties = when (propertiesResult) {
is com.shieldai.android.data.remote.ApiResult.Success -> propertiesResult.data
else -> emptyList()
}
val removals = when (removalsResult) {
is com.shieldai.android.data.remote.ApiResult.Success -> removalsResult.data
else -> emptyList()
}
val threatScore = calculateThreatScore(alerts)
val unreadCount = alerts.count { !it.read }
val recentAlerts = alerts.sortedByDescending { it.createdAt }
.take(5)
_uiState.value = _uiState.value.copy(
isLoading = false,
threatScore = threatScore,
recentAlerts = recentAlerts,
unreadCount = unreadCount,
watchlistCount = watchlist.size,
enrollmentCount = enrollments.size,
spamRulesCount = rules.size,
propertiesCount = properties.size,
removalsCount = removals.size
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load dashboard data"
)
}
}
}
fun markAlertRead(alertId: String) {
viewModelScope.launch {
alertRepo.markRead(alertId)
}
}
private fun calculateThreatScore(alerts: List<Alert>): Int {
if (alerts.isEmpty()) return 0
val score = alerts.sumOf {
when (it.severity.lowercase()) {
"critical" -> 25
"high" -> 15
"medium" -> 8
"low" -> 3
else -> 1
}
}
return minOf(score.coerceAtMost(100), 100)
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return DashboardViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,84 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.Property
import com.shieldai.android.data.repository.HomeTitleRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class HomeTitleUiState(
val properties: List<Property> = emptyList(),
val isLoading: Boolean = true,
val isAdding: Boolean = false,
val error: String? = null
)
class HomeTitleViewModel : ViewModel() {
private val _uiState = MutableStateFlow(HomeTitleUiState())
val uiState: StateFlow<HomeTitleUiState> = _uiState.asStateFlow()
private val repo: HomeTitleRepository by lazy {
RepositoryModule.provideHomeTitleRepository(ShieldAIApp.instance)
}
init {
loadProperties()
}
fun refresh() {
loadProperties(forceRefresh = true)
}
private fun loadProperties(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val result = repo.getProperties(forceRefresh)
if (result is com.shieldai.android.data.remote.ApiResult.Success) {
_uiState.value = _uiState.value.copy(
isLoading = false,
properties = result.data
)
} else {
_uiState.value = _uiState.value.copy(isLoading = false)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load properties"
)
}
}
}
fun addProperty(address: String, type: String = "residential") {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isAdding = true, error = null)
val result = repo.addProperty(address, type)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isAdding = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isAdding = false)
loadProperties(forceRefresh = true)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return HomeTitleViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.BrokerListing
import com.shieldai.android.data.model.RemovalRequest
import com.shieldai.android.data.repository.RemoveBrokersRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class RemoveBrokersUiState(
val listings: List<BrokerListing> = emptyList(),
val removalRequests: List<RemovalRequest> = emptyList(),
val isLoading: Boolean = true,
val isCreating: Boolean = false,
val error: String? = null
)
class RemoveBrokersViewModel : ViewModel() {
private val _uiState = MutableStateFlow(RemoveBrokersUiState())
val uiState: StateFlow<RemoveBrokersUiState> = _uiState.asStateFlow()
private val repo: RemoveBrokersRepository by lazy {
RepositoryModule.provideRemoveBrokersRepository(ShieldAIApp.instance)
}
init {
loadData()
}
fun refresh() {
loadData(forceRefresh = true)
}
private fun loadData(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val listingsResult = repo.getListings(forceRefresh)
val requestsResult = repo.getRemovalRequests(forceRefresh)
val listings = if (listingsResult is com.shieldai.android.data.remote.ApiResult.Success) {
listingsResult.data
} else emptyList()
val requests = if (requestsResult is com.shieldai.android.data.remote.ApiResult.Success) {
requestsResult.data
} else emptyList()
_uiState.value = _uiState.value.copy(
isLoading = false,
listings = listings,
removalRequests = requests
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load data"
)
}
}
}
fun createRemovalRequest(listingId: String, notes: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isCreating = true, error = null)
val result = repo.createRemovalRequest(listingId, notes)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isCreating = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isCreating = false)
loadData(forceRefresh = true)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RemoveBrokersViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,109 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.Subscription
import com.shieldai.android.data.model.User
import com.shieldai.android.data.repository.SubscriptionRepository
import com.shieldai.android.data.repository.UserRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class SettingsUiState(
val user: User? = null,
val subscription: Subscription? = null,
val isLoading: Boolean = true,
val notificationsEnabled: Boolean = true,
val darkModeEnabled: Boolean = false,
val biometricEnabled: Boolean = false,
val error: String? = null
)
class SettingsViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
private val userRepo: UserRepository by lazy {
RepositoryModule.provideUserRepository(ShieldAIApp.instance)
}
private val subscriptionRepo: SubscriptionRepository by lazy {
RepositoryModule.provideSubscriptionRepository(ShieldAIApp.instance)
}
init {
loadSettings()
}
fun refresh() {
loadSettings(forceRefresh = true)
}
private fun loadSettings(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
try {
val userResult = userRepo.getMe(forceRefresh)
val subResult = subscriptionRepo.getSubscription()
val user = if (userResult is com.shieldai.android.data.remote.ApiResult.Success) {
userResult.data
} else null
val subscription = if (subResult is com.shieldai.android.data.remote.ApiResult.Success) {
subResult.data
} else null
_uiState.value = _uiState.value.copy(
isLoading = false,
user = user,
subscription = subscription
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load settings"
)
}
}
}
fun toggleNotifications(enabled: Boolean) {
_uiState.value = _uiState.value.copy(notificationsEnabled = enabled)
}
fun toggleDarkMode(enabled: Boolean) {
_uiState.value = _uiState.value.copy(darkModeEnabled = enabled)
}
fun toggleBiometric(enabled: Boolean) {
_uiState.value = _uiState.value.copy(biometricEnabled = enabled)
}
fun updateProfile(name: String? = null, phone: String? = null) {
viewModelScope.launch {
userRepo.updateProfile(name, phone)
loadSettings(forceRefresh = true)
}
}
fun upgradeSubscription() {
viewModelScope.launch {
subscriptionRepo.updateSubscription("Premium")
loadSettings(forceRefresh = true)
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SettingsViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,98 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.SpamRule
import com.shieldai.android.data.repository.SpamShieldRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class SpamShieldUiState(
val rules: List<SpamRule> = emptyList(),
val totalBlocked: Int = 0,
val totalFlagged: Int = 0,
val activeRules: Int = 0,
val isLoading: Boolean = true,
val isCreating: Boolean = false,
val error: String? = null
)
class SpamShieldViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SpamShieldUiState())
val uiState: StateFlow<SpamShieldUiState> = _uiState.asStateFlow()
private val repo: SpamShieldRepository by lazy {
RepositoryModule.provideSpamShieldRepository(ShieldAIApp.instance)
}
init {
loadRules()
}
fun refresh() {
loadRules(forceRefresh = true)
}
private fun loadRules(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val result = repo.getRules(forceRefresh)
if (result is com.shieldai.android.data.remote.ApiResult.Success) {
val stats = repo.getStats()
_uiState.value = _uiState.value.copy(
isLoading = false,
rules = result.data,
totalBlocked = stats.totalBlocked,
totalFlagged = stats.totalFlagged,
activeRules = stats.activeRules
)
} else {
_uiState.value = _uiState.value.copy(isLoading = false)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load rules"
)
}
}
}
fun createRule(pattern: String, action: String, description: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isCreating = true, error = null)
val result = repo.createRule(pattern, action, description)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isCreating = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isCreating = false)
loadRules(forceRefresh = true)
}
}
}
fun toggleRule(id: String, enabled: Boolean) {
viewModelScope.launch {
repo.toggleRule(id, enabled)
loadRules(forceRefresh = true)
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SpamShieldViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,96 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.data.model.VoiceEnrollment
import com.shieldai.android.data.repository.VoicePrintRepository
import com.shieldai.android.di.RepositoryModule
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class VoicePrintUiState(
val enrollments: List<VoiceEnrollment> = emptyList(),
val isLoading: Boolean = true,
val isEnrolling: Boolean = false,
val error: String? = null
)
class VoicePrintViewModel : ViewModel() {
private val _uiState = MutableStateFlow(VoicePrintUiState())
val uiState: StateFlow<VoicePrintUiState> = _uiState.asStateFlow()
private val repo: VoicePrintRepository by lazy {
RepositoryModule.provideVoicePrintRepository(ShieldAIApp.instance)
}
init {
loadEnrollments()
}
fun refresh() {
loadEnrollments(forceRefresh = true)
}
private fun loadEnrollments(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val result = if (forceRefresh) {
repo.getEnrollments()
} else {
repo.getEnrollments()
}
if (result is com.shieldai.android.data.remote.ApiResult.Success) {
_uiState.value = _uiState.value.copy(
isLoading = false,
enrollments = result.data
)
} else {
_uiState.value = _uiState.value.copy(isLoading = false)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load enrollments"
)
}
}
}
fun createEnrollment(name: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isEnrolling = true, error = null)
val result = repo.createEnrollment(name)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isEnrolling = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isEnrolling = false)
loadEnrollments(forceRefresh = true)
}
}
}
fun deleteEnrollment(id: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(
enrollments = _uiState.value.enrollments.filter { it.id != id }
)
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return VoicePrintViewModel() as T
}
}
}
}