holy moly thats a lotta damage

This commit is contained in:
2026-05-25 22:10:19 -04:00
parent c01c1a5636
commit b62ab77fbe
47 changed files with 1444 additions and 129 deletions

View File

@@ -0,0 +1,264 @@
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.HomeTitleViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeTitleScreen(
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: HomeTitleViewModel = viewModel(factory = HomeTitleViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var showAddSheet by remember { mutableStateOf(false) }
var newAddress by remember { mutableStateOf("") }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("HomeTitle", 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 property"
)
}
}
}
) { paddingValues ->
when {
uiState.isLoading && uiState.properties.isEmpty() -> {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
uiState.properties.isEmpty() -> {
ShieldEmptyState(
title = "No properties",
description = "Add properties to monitor for title fraud",
actionButton = {
ShieldButton(
text = "Add Property",
onClick = { showAddSheet = true },
variant = ShieldButtonVariant.Primary
)
},
modifier = Modifier.padding(paddingValues)
)
}
else -> {
HomeTitleContent(
uiState = uiState,
modifier = Modifier.padding(paddingValues)
)
}
}
if (showAddSheet) {
AddPropertySheet(
onDismiss = {
showAddSheet = false
newAddress = ""
},
onAdd = {
viewModel.addProperty(newAddress)
showAddSheet = false
newAddress = ""
},
address = newAddress,
onAddressChange = { newAddress = it },
isLoading = uiState.isAdding
)
}
}
}
@Composable
private fun HomeTitleContent(
uiState: HomeTitleViewModel.HomeTitleUiState,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(
text = "Properties (${uiState.properties.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.properties) { property ->
PropertyCard(property)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
private fun PropertyCard(property: com.shieldai.android.data.model.Property) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = property.address,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
property.ownerName?.let {
Text(
text = "Owner: $it",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ShieldBadge(
text = property.type,
variant = BadgeVariant.Info
)
property.county?.let {
ShieldBadge(
text = it,
variant = BadgeVariant.Default
)
}
}
}
ShieldBadge(
text = property.status,
variant = if (property.status == "monitored") BadgeVariant.Success
else BadgeVariant.Default
)
}
property.updatedAt?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Updated: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AddPropertySheet(
onDismiss: () -> Unit,
onAdd: () -> Unit,
address: String,
onAddressChange: (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 Property",
style = MaterialTheme.typography.titleLarge
)
ShieldTextField(
value = address,
onValueChange = onAddressChange,
label = "Property address",
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 = address.isNotBlank(),
loading = isLoading
)
}
}
}
}

View File

@@ -0,0 +1,327 @@
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.RemoveBrokersViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RemoveBrokersScreen(
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: RemoveBrokersViewModel = viewModel(factory = RemoveBrokersViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var showCreateSheet by remember { mutableStateOf(false) }
var selectedListingId by remember { mutableStateOf("") }
var selectedListingName by remember { mutableStateOf("") }
var notes by remember { mutableStateOf("") }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("RemoveBrokers", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
TextButton(onClick = onBack) { Text("Back") }
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
if (!showCreateSheet) {
FloatingActionButton(onClick = { showCreateSheet = true }) {
Icon(
painter = painterResource(R.drawable.ic_dashboard),
contentDescription = "Start removal"
)
}
}
}
) { paddingValues ->
when {
uiState.isLoading && uiState.listings.isEmpty() -> {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
uiState.listings.isEmpty() && uiState.removalRequests.isEmpty() -> {
ShieldEmptyState(
title = "No listings",
description = "No broker listings found. Start a removal request to get started.",
actionButton = {
ShieldButton(
text = "Start Removal",
onClick = { showCreateSheet = true },
variant = ShieldButtonVariant.Primary
)
},
modifier = Modifier.padding(paddingValues)
)
}
else -> {
RemoveBrokersContent(
uiState = uiState,
modifier = Modifier.padding(paddingValues)
)
}
}
if (showCreateSheet) {
CreateRemovalSheet(
onDismiss = {
showCreateSheet = false
selectedListingId = ""
selectedListingName = ""
notes = ""
},
onCreate = {
viewModel.createRemovalRequest(selectedListingId, notes.ifBlank { null })
showCreateSheet = false
selectedListingId = ""
selectedListingName = ""
notes = ""
},
listingName = selectedListingName,
onListingNameChange = { selectedListingName = it },
notes = notes,
onNotesChange = { notes = it },
isLoading = uiState.isCreating
)
}
}
}
@Composable
private fun RemoveBrokersContent(
uiState: RemoveBrokersViewModel.RemoveBrokersUiState,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (uiState.listings.isNotEmpty()) {
item {
Text(
text = "Broker Listings (${uiState.listings.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.listings) { listing ->
ListingCard(listing)
Spacer(modifier = Modifier.height(8.dp))
}
}
if (uiState.removalRequests.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Removal Requests (${uiState.removalRequests.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.removalRequests) { request ->
RemovalRequestCard(request)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun ListingCard(listing: com.shieldai.android.data.model.BrokerListing) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = listing.brokerName,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
)
ShieldBadge(
text = listing.status,
variant = if (listing.status == "active") BadgeVariant.Warning
else BadgeVariant.Default
)
}
listing.propertyAddress?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
listing.dateFound?.let {
Text(
text = "Found: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
private fun RemovalRequestCard(request: com.shieldai.android.data.model.RemovalRequest) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Request #${request.id.take(8)}",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
)
ShieldBadge(
text = request.status,
variant = when (request.status.lowercase()) {
"completed" -> BadgeVariant.Success
"pending" -> BadgeVariant.Warning
"in_progress" -> BadgeVariant.Info
else -> BadgeVariant.Default
}
)
}
request.submittedDate?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Submitted: $it",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
request.notes?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreateRemovalSheet(
onDismiss: () -> Unit,
onCreate: () -> Unit,
listingName: String,
onListingNameChange: (String) -> Unit,
notes: String,
onNotesChange: (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 = "Start Removal Request",
style = MaterialTheme.typography.titleLarge
)
ShieldTextField(
value = listingName,
onValueChange = onListingNameChange,
label = "Broker / Listing name",
modifier = Modifier.fillMaxWidth()
)
ShieldTextField(
value = notes,
onValueChange = onNotesChange,
label = "Notes (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 = "Submit",
onClick = onCreate,
variant = ShieldButtonVariant.Primary,
modifier = Modifier.weight(1f),
enabled = listingName.isNotBlank(),
loading = isLoading
)
}
}
}
}

View File

@@ -0,0 +1,338 @@
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.Switch
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.SpamShieldViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SpamShieldScreen(
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: SpamShieldViewModel = viewModel(factory = SpamShieldViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var showCreateSheet by remember { mutableStateOf(false) }
var newPattern by remember { mutableStateOf("") }
var newAction by remember { mutableStateOf("block") }
var newDescription by remember { mutableStateOf("") }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("SpamShield", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
TextButton(onClick = onBack) { Text("Back") }
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
if (!showCreateSheet) {
FloatingActionButton(onClick = { showCreateSheet = true }) {
Icon(
painter = painterResource(R.drawable.ic_dashboard),
contentDescription = "Create rule"
)
}
}
}
) { paddingValues ->
when {
uiState.isLoading && uiState.rules.isEmpty() -> {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
else -> {
SpamShieldContent(
uiState = uiState,
modifier = Modifier.padding(paddingValues)
)
}
}
if (showCreateSheet) {
CreateRuleSheet(
onDismiss = {
showCreateSheet = false
newPattern = ""
newDescription = ""
},
onCreate = {
viewModel.createRule(newPattern, newAction, newDescription.ifBlank { null })
showCreateSheet = false
newPattern = ""
newDescription = ""
},
pattern = newPattern,
onPatternChange = { newPattern = it },
action = newAction,
onActionChange = { newAction = it },
description = newDescription,
onDescriptionChange = { newDescription = it },
isLoading = uiState.isCreating
)
}
}
}
@Composable
private fun SpamShieldContent(
uiState: SpamShieldViewModel.SpamShieldUiState,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
SpamStatsRow(
blocked = uiState.totalBlocked,
flagged = uiState.totalFlagged,
active = uiState.activeRules
)
}
if (uiState.rules.isNotEmpty()) {
item {
Text(
text = "Rules (${uiState.rules.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.rules) { rule ->
RuleCard(rule) { enabled ->
viewModel.toggleRule(rule.id, enabled)
}
Spacer(modifier = Modifier.height(8.dp))
}
} else {
item {
ShieldEmptyState(
title = "No rules",
description = "Create spam filtering rules to protect your phone",
actionButton = {
ShieldButton(
text = "Create Rule",
onClick = { /* handled by parent */ },
variant = ShieldButtonVariant.Primary
)
}
)
}
}
}
}
@Composable
private fun SpamStatsRow(
blocked: Int,
flagged: Int,
active: Int
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
StatCard("Blocked", blocked, modifier = Modifier.weight(1f))
StatCard("Flagged", flagged, modifier = Modifier.weight(1f))
StatCard("Active", active, modifier = Modifier.weight(1f))
}
}
@Composable
private fun StatCard(
label: String,
value: Int,
modifier: Modifier = Modifier
) {
ShieldCard(
modifier = modifier
) {
Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally
) {
Text(
text = "$value",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
private fun RuleCard(
rule: com.shieldai.android.data.model.SpamRule,
onToggle: (Boolean) -> Unit
) {
ShieldCard(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = rule.pattern,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ShieldBadge(
text = rule.action,
variant = if (rule.action == "block") com.shieldai.android.ui.components.BadgeVariant.Error
else com.shieldai.android.ui.components.BadgeVariant.Warning
)
if (rule.priority > 0) {
ShieldBadge(
text = "P${rule.priority}",
variant = com.shieldai.android.ui.components.BadgeVariant.Info
)
}
}
rule.description?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Switch(
checked = rule.enabled,
onCheckedChange = onToggle
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreateRuleSheet(
onDismiss: () -> Unit,
onCreate: () -> Unit,
pattern: String,
onPatternChange: (String) -> Unit,
action: String,
onActionChange: (String) -> Unit,
description: String,
onDescriptionChange: (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 = "Create Spam Rule",
style = MaterialTheme.typography.titleLarge
)
ShieldTextField(
value = pattern,
onValueChange = onPatternChange,
label = "Pattern (phone number or keyword)",
modifier = Modifier.fillMaxWidth()
)
ShieldTextField(
value = action,
onValueChange = onActionChange,
label = "Action (block, flag, log)",
modifier = Modifier.fillMaxWidth()
)
ShieldTextField(
value = description,
onValueChange = onDescriptionChange,
label = "Description (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 = "Create",
onClick = onCreate,
variant = ShieldButtonVariant.Primary,
modifier = Modifier.weight(1f),
enabled = pattern.isNotBlank(),
loading = isLoading
)
}
}
}
}

View File

@@ -0,0 +1,346 @@
package com.shieldai.android.ui.screens.settings
import androidx.compose.foundation.clickable
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.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
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.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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.shieldai.android.ui.components.ShieldAvatar
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.viewmodel.AuthViewModel
import com.shieldai.android.viewmodel.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBack: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory),
authViewModel: AuthViewModel = viewModel(factory = AuthViewModel.Factory)
) {
val uiState by viewModel.uiState.collectAsState()
var showLogoutDialog by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text("Settings", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
TextButton(onClick = onBack) { Text("Back") }
},
scrollBehavior = scrollBehavior
)
}
) { paddingValues ->
when {
uiState.isLoading -> {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
uiState.user == null -> {
ShieldEmptyState(
title = "Failed to load settings",
description = uiState.error ?: "Unable to load your settings",
actionButton = {
TextButton(onClick = { viewModel.refresh() }) {
Text("Retry")
}
},
modifier = Modifier.padding(paddingValues)
)
}
else -> {
SettingsContent(
uiState = uiState,
onToggleNotifications = { viewModel.toggleNotifications(it) },
onToggleDarkMode = { viewModel.toggleDarkMode(it) },
onToggleBiometric = { viewModel.toggleBiometric(it) },
onUpgradeSubscription = { viewModel.upgradeSubscription() },
onShowLogoutDialog = { showLogoutDialog = true },
modifier = Modifier.padding(paddingValues)
)
}
}
if (showLogoutDialog) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { showLogoutDialog = false },
title = { Text("Logout") },
text = { Text("Are you sure you want to logout?") },
confirmButton = {
TextButton(
onClick = {
authViewModel.logout()
showLogoutDialog = false
}
) {
Text(
text = "Logout",
color = MaterialTheme.colorScheme.error
)
}
},
dismissButton = {
TextButton(onClick = { showLogoutDialog = false }) {
Text("Cancel")
}
}
)
}
}
}
@Composable
private fun SettingsContent(
uiState: SettingsViewModel.SettingsUiState,
onToggleNotifications: (Boolean) -> Unit,
onToggleDarkMode: (Boolean) -> Unit,
onToggleBiometric: (Boolean) -> Unit,
onUpgradeSubscription: () -> Unit,
onShowLogoutDialog: () -> Unit,
modifier: Modifier = Modifier
) {
val user = uiState.user!!
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
AccountSection(user)
}
item {
SubscriptionSection(
subscription = uiState.subscription,
onUpgrade = onUpgradeSubscription
)
}
item {
PreferencesSection(
notificationsEnabled = uiState.notificationsEnabled,
darkModeEnabled = uiState.darkModeEnabled,
biometricEnabled = uiState.biometricEnabled,
onToggleNotifications = onToggleNotifications,
onToggleDarkMode = onToggleDarkMode,
onToggleBiometric = onToggleBiometric
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
ShieldButton(
text = "Logout",
onClick = onShowLogoutDialog,
variant = ShieldButtonVariant.Danger,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun AccountSection(user: com.shieldai.android.data.model.User) {
Column {
Text(
text = "Account",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ShieldAvatar(
name = user.name,
avatarUrl = user.avatarUrl
)
Column {
Text(
text = user.name,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (user.emailVerified) {
ShieldBadge(text = "Email verified", variant = com.shieldai.android.ui.components.BadgeVariant.Success)
}
if (user.phoneVerified) {
ShieldBadge(text = "Phone verified", variant = com.shieldai.android.ui.components.BadgeVariant.Success)
}
}
}
}
}
}
@Composable
private fun SubscriptionSection(
subscription: com.shieldai.android.data.model.Subscription?,
onUpgrade: () -> Unit
) {
Column {
Text(
text = "Subscription",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
ShieldCard {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = subscription?.plan ?: "Free",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
Text(
text = subscription?.status ?: "No subscription",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
ShieldButton(
text = "Upgrade",
onClick = onUpgrade,
variant = ShieldButtonVariant.Secondary,
size = com.shieldai.android.ui.components.ShieldButtonSize.Small
)
}
}
}
}
@Composable
private fun PreferencesSection(
notificationsEnabled: Boolean,
darkModeEnabled: Boolean,
biometricEnabled: Boolean,
onToggleNotifications: (Boolean) -> Unit,
onToggleDarkMode: (Boolean) -> Unit,
onToggleBiometric: (Boolean) -> Unit
) {
Column {
Text(
text = "Preferences",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
ShieldCard {
Column {
SettingRow(
title = "Notifications",
description = "Receive push notifications for alerts",
checked = notificationsEnabled,
onCheckedChange = onToggleNotifications
)
Divider()
SettingRow(
title = "Dark Mode",
description = "Use dark theme",
checked = darkModeEnabled,
onCheckedChange = onToggleDarkMode
)
Divider()
SettingRow(
title = "Biometric Auth",
description = "Use fingerprint or face unlock",
checked = biometricEnabled,
onCheckedChange = onToggleBiometric
)
}
}
}
}
@Composable
private fun SettingRow(
title: String,
description: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp)
.clickable { onCheckedChange(!checked) },
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}