rebranding work
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user