Compare commits

...

3 Commits

Author SHA1 Message Date
3d246af3f7 rebranding 2026-05-25 22:49:37 -04:00
b62ab77fbe holy moly thats a lotta damage 2026-05-25 22:10:19 -04:00
c01c1a5636 rebranding work 2026-05-25 21:53:01 -04:00
282 changed files with 5738 additions and 808 deletions

View File

@@ -1,4 +1,4 @@
DATABASE_URL="postgresql://shieldai:shieldai_dev@localhost:5432/shieldai"
DATABASE_URL="postgresql://kordant:kordant_dev@localhost:5432/kordant"
REDIS_URL="redis://localhost:6379"
PORT=3000
LOG_LEVEL=info
@@ -7,7 +7,7 @@ RESEND_API_KEY=""
AWS_REGION="us-east-1"
# Datadog APM Configuration
DD_SERVICE="shieldai-api"
DD_SERVICE="kordant-api"
DD_ENV="development"
DD_VERSION="0.1.0"
DD_TRACE_ENABLED="true"

View File

@@ -7,7 +7,7 @@ RESEND_API_KEY=""
# Docker (for deployment)
DOCKER_TAG=latest
GITHUB_REPOSITORY_OWNER=shieldai
GITHUB_REPOSITORY_OWNER=kordant
# Server
PORT=3000

View File

@@ -58,13 +58,13 @@ jobs:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: shieldai
POSTGRES_USER: shieldai
POSTGRES_PASSWORD: shieldai_dev
POSTGRES_DB: kordant
POSTGRES_USER: kordant
POSTGRES_PASSWORD: kordant_dev
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U shieldai"
--health-cmd "pg_isready -U kordant"
--health-interval 5s
--health-timeout 5s
--health-retries 5
@@ -92,14 +92,14 @@ jobs:
- name: Run tests
run: pnpm test
env:
DATABASE_URL: "postgresql://shieldai:shieldai_dev@localhost:5432/shieldai"
DATABASE_URL: "postgresql://kordant:kordant_dev@localhost:5432/kordant"
REDIS_URL: "redis://localhost:6379"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
flags: unittests
name: shieldai-coverage
name: kordant-coverage
fail_on_empty: false
docker-build:
@@ -115,7 +115,7 @@ jobs:
with:
context: .
push: false
tags: shieldai:${{ github.sha }}
tags: kordant:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -52,7 +52,7 @@ jobs:
terraform_version: "~> 1.5"
- name: Terraform Init
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
run: terraform init -backend-config="bucket=shieldai-${{ needs.detect-environment.outputs.environment }}-terraform-state"
run: terraform init -backend-config="bucket=kordant-${{ needs.detect-environment.outputs.environment }}-terraform-state"
- name: Terraform Plan
id: plan
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
@@ -113,8 +113,8 @@ jobs:
file: ${{ matrix.dockerfile }}
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/shieldai-${{ matrix.name }}:${{ steps.tag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/shieldai-${{ matrix.name }}:latest
ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.name }}:${{ steps.tag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.name }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -137,8 +137,8 @@ jobs:
aws-region: us-east-1
- name: Update ECS Service
run: |
IMAGE="ghcr.io/${{ github.repository_owner }}/shieldai-${{ matrix.service }}:${{ needs.detect-environment.outputs.tag }}"
CLUSTER="shieldai-${{ needs.detect-environment.outputs.environment }}"
IMAGE="ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.service }}:${{ needs.detect-environment.outputs.tag }}"
CLUSTER="kordant-${{ needs.detect-environment.outputs.environment }}"
SERVICE="${{ matrix.service }}"
TASK_DEF=$(aws ecs describe-task-definition \
@@ -181,7 +181,7 @@ jobs:
id: health
run: |
ENV="${{ needs.detect-environment.outputs.environment }}"
CLUSTER="shieldai-${ENV}"
CLUSTER="kordant-${ENV}"
ALB_DNS=$(aws elbv2 describe-load-balancers \
--query "LoadBalancers[?contains(LoadBalancerName, '${CLUSTER}-alb')].DNSName" \
@@ -230,7 +230,7 @@ jobs:
aws-region: us-east-1
- name: Rollback ECS Service
run: |
CLUSTER="shieldai-${{ needs.detect-environment.outputs.environment }}"
CLUSTER="kordant-${{ needs.detect-environment.outputs.environment }}"
SERVICE="${{ matrix.service }}"
aws ecs update-service \

View File

@@ -1,8 +1,8 @@
# ShieldAI
# Kordant
**Multi-layered consumer identity protection against predatory AI-driven scams.**
ShieldAI combines three detection engines — voice cloning detection, dark web monitoring, and real-time spam classification — to give consumers proactive defense against modern identity fraud.
Kordant combines three detection engines — voice cloning detection, dark web monitoring, and real-time spam classification — to give consumers proactive defense against modern identity fraud.
---
@@ -10,13 +10,13 @@ ShieldAI combines three detection engines — voice cloning detection, dark web
Scammers are weaponizing AI at scale: voice clones that sound exactly like your family, hyper-personalized phishing messages that bypass filters, and synthetic identities that exploit stolen data within hours of a breach. Legacy credit monitoring is reactive — it tells you after the damage is done.
ShieldAI flips the model. We detect the scam _as it happens_:
Kordant flips the model. We detect the scam _as it happens_:
- **VoicePrint** analyzes inbound calls in real time to flag synthetic AI-generated voices before you're socially engineered.
- **DarkWatch** continuously monitors dark web forums, breach databases, and data broker caches — notifying you the moment your credentials, phone, or SSN surface.
- **SpamShield** intercepts and classifies spam calls and SMS at the network level, blocking threats before they reach your phone.
Backed by ML models (ECAPA-TDNN, BERT) and a real-time alert pipeline, ShieldAI gives consumers enterprise-grade threat detection for their personal life.
Backed by ML models (ECAPA-TDNN, BERT) and a real-time alert pipeline, Kordant gives consumers enterprise-grade threat detection for their personal life.
---
@@ -173,10 +173,10 @@ This launches the API server, all microservices, and the web frontend concurrent
pnpm build
# Build individual Docker images
docker build -f packages/api/Dockerfile -t shieldai-api .
docker build -f services/spamshield/Dockerfile -t shieldai-spamshield .
docker build -f services/darkwatch/Dockerfile -t shieldai-darkwatch .
docker build -f services/voiceprint/Dockerfile -t shieldai-voiceprint .
docker build -f packages/api/Dockerfile -t kordant-api .
docker build -f services/spamshield/Dockerfile -t kordant-spamshield .
docker build -f services/darkwatch/Dockerfile -t kordant-darkwatch .
docker build -f services/voiceprint/Dockerfile -t kordant-voiceprint .
```
---
@@ -191,10 +191,10 @@ pnpm test
pnpm test:coverage
# Individual service tests
pnpm test --filter @shieldai/spamshield
pnpm test --filter @shieldai/darkwatch
pnpm test --filter @shieldai/voiceprint
pnpm test --filter @shieldai/hometitle
pnpm test --filter @kordant/spamshield
pnpm test --filter @kordant/darkwatch
pnpm test --filter @kordant/voiceprint
pnpm test --filter @kordant/hometitle
# Integration & E2E
cd packages/integration-tests && pnpm test
@@ -242,7 +242,7 @@ See `infra/README.md` and `infra/ROLLBACK.md` for detailed operational runbooks.
## Project Structure
```
shieldai/
kordant/
├── packages/ # Shared libraries (20 packages)
│ ├── api/ # Fastify API server
│ ├── core/ # Core shared logic
@@ -292,7 +292,7 @@ shieldai/
| Push Notifications | `docs/PUSH_NOTIFICATIONS_SETUP.md` |
| Mobile Push Integration | `docs/MOBILE_PUSH_INTEGRATION.md` |
| Mixpanel Analytics | `docs/MIXPANEL_ANALYTICS.md` |
| Code Review Workflow | `shieldai-workflow.md` |
| Code Review Workflow | `kordant-workflow.md` |
---

View File

@@ -5,7 +5,7 @@ plugins {
}
android {
namespace = "com.shieldai.android"
namespace = "com.kordant.android"
compileSdk {
version = release(36) {
minorApiLevel = 1
@@ -13,7 +13,7 @@ android {
}
defaultConfig {
applicationId = "com.shieldai.android"
applicationId = "com.kordant.android"
minSdk = 26
targetSdk = 36
versionCode = 1
@@ -22,8 +22,8 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:3000\"")
buildConfigField("String", "API_STAGING_URL", "\"https://staging.api.shieldai.com\"")
buildConfigField("String", "API_PRODUCTION_URL", "\"https://api.shieldai.com\"")
buildConfigField("String", "API_STAGING_URL", "\"https://staging.api.kordant.com\"")
buildConfigField("String", "API_PRODUCTION_URL", "\"https://api.kordant.com\"")
}
buildTypes {
@@ -36,7 +36,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField("String", "API_BASE_URL", "\"https://api.shieldai.com\"")
buildConfigField("String", "API_BASE_URL", "\"https://api.kordant.com\"")
}
}
compileOptions {

View File

@@ -18,7 +18,7 @@
errorLine1="distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/wrapper/gradle-wrapper.properties"
file="$HOME/Code/Kordant/android/Kordant/gradle/wrapper/gradle-wrapper.properties"
line="5"
column="17"/>
</issue>
@@ -29,7 +29,7 @@
errorLine1="agp = &quot;9.1.1&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="2"
column="7"/>
</issue>
@@ -40,7 +40,7 @@
errorLine1="coreKtx = &quot;1.10.1&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="3"
column="11"/>
</issue>
@@ -51,7 +51,7 @@
errorLine1="junitVersion = &quot;1.1.5&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="5"
column="16"/>
</issue>
@@ -62,7 +62,7 @@
errorLine1="espressoCore = &quot;3.5.1&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="6"
column="16"/>
</issue>
@@ -73,7 +73,7 @@
errorLine1="lifecycleRuntimeKtx = &quot;2.6.1&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="7"
column="23"/>
</issue>
@@ -84,7 +84,7 @@
errorLine1="activityCompose = &quot;1.8.0&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="8"
column="19"/>
</issue>
@@ -95,7 +95,7 @@
errorLine1="navigationCompose = &quot;2.7.7&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="9"
column="21"/>
</issue>
@@ -106,7 +106,7 @@
errorLine1="composeBom = &quot;2025.12.00&quot;"
errorLine2=" ~~~~~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="11"
column="14"/>
</issue>
@@ -117,7 +117,7 @@
errorLine1="securityCrypto = &quot;1.1.0-alpha06&quot;"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="13"
column="18"/>
</issue>
@@ -128,7 +128,7 @@
errorLine1="playServicesAuth = &quot;21.0.0&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="15"
column="20"/>
</issue>
@@ -139,7 +139,7 @@
errorLine1="work = &quot;2.9.1&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="23"
column="8"/>
</issue>
@@ -150,7 +150,7 @@
errorLine1="work = &quot;2.9.1&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="23"
column="8"/>
</issue>
@@ -161,7 +161,7 @@
errorLine1="kotlin = &quot;2.2.10&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="10"
column="10"/>
</issue>
@@ -172,7 +172,7 @@
errorLine1="kotlin = &quot;2.2.10&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="10"
column="10"/>
</issue>
@@ -183,7 +183,7 @@
errorLine1="okhttp = &quot;4.12.0&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="16"
column="10"/>
</issue>
@@ -194,7 +194,7 @@
errorLine1="okhttp = &quot;4.12.0&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="16"
column="10"/>
</issue>
@@ -205,7 +205,7 @@
errorLine1="gson = &quot;2.10.1&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="17"
column="8"/>
</issue>
@@ -216,7 +216,7 @@
errorLine1="lottieCompose = &quot;6.4.0&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="18"
column="17"/>
</issue>
@@ -227,7 +227,7 @@
errorLine1="coroutinesTest = &quot;1.7.3&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="19"
column="18"/>
</issue>
@@ -238,7 +238,7 @@
errorLine1="retrofit = &quot;2.11.0&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="20"
column="12"/>
</issue>
@@ -249,7 +249,7 @@
errorLine1="kotlinxSerializationJson = &quot;1.7.3&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="22"
column="28"/>
</issue>
@@ -260,7 +260,7 @@
errorLine1="truth = &quot;1.4.4&quot;"
errorLine2=" ~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="24"
column="9"/>
</issue>
@@ -271,7 +271,7 @@
errorLine1="mockwebserver = &quot;4.12.0&quot;"
errorLine2=" ~~~~~~~~">
<location
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
line="25"
column="17"/>
</issue>
@@ -279,10 +279,10 @@
<issue
id="LocalContextGetResourceValueCall"
message="Querying resource values using LocalContext.current"
errorLine1=" .requestIdToken(context.getString(com.shieldai.android.R.string.default_web_client_id))"
errorLine1=" .requestIdToken(context.getString(com.kordant.android.R.string.default_web_client_id))"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/ui/screens/auth/LoginScreen.kt"
file="src/main/java/com/kordant/android/ui/screens/auth/LoginScreen.kt"
line="56"
column="29"/>
</issue>
@@ -293,7 +293,7 @@
errorLine1=" private var userRepository: UserRepository? = null"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
line="11"
column="5"/>
</issue>
@@ -304,7 +304,7 @@
errorLine1=" private var darkWatchRepository: DarkWatchRepository? = null"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
line="12"
column="5"/>
</issue>
@@ -315,7 +315,7 @@
errorLine1=" private var voicePrintRepository: VoicePrintRepository? = null"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
line="13"
column="5"/>
</issue>
@@ -326,7 +326,7 @@
errorLine1=" private var alertRepository: AlertRepository? = null"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
line="14"
column="5"/>
</issue>
@@ -337,7 +337,7 @@
errorLine1=" private var subscriptionRepository: SubscriptionRepository? = null"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
line="15"
column="5"/>
</issue>
@@ -480,7 +480,7 @@
errorLine1=" securePrefs.edit()"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/data/repository/AuthRepository.kt"
file="src/main/java/com/kordant/android/data/repository/AuthRepository.kt"
line="144"
column="9"/>
</issue>
@@ -491,7 +491,7 @@
errorLine1=" securePrefs.edit()"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/data/repository/AuthRepository.kt"
file="src/main/java/com/kordant/android/data/repository/AuthRepository.kt"
line="155"
column="9"/>
</issue>
@@ -502,7 +502,7 @@
errorLine1=" prefs.edit().putBoolean(&quot;biometric_enabled&quot;, enabled).apply()"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/java/com/shieldai/android/ui/screens/auth/BiometricAuthScreen.kt"
file="src/main/java/com/kordant/android/ui/screens/auth/BiometricAuthScreen.kt"
line="88"
column="5"/>
</issue>

View File

@@ -1,4 +1,4 @@
package com.shieldai.android
package com.kordant.android
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.ui.Modifier
@@ -10,15 +10,15 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import com.shieldai.android.ui.components.BadgeVariant
import com.shieldai.android.ui.components.ComponentShowcase
import com.shieldai.android.ui.components.InputType
import com.shieldai.android.ui.components.ShieldBadge
import com.shieldai.android.ui.components.ShieldButton
import com.shieldai.android.ui.components.ShieldButtonSize
import com.shieldai.android.ui.components.ShieldButtonVariant
import com.shieldai.android.ui.components.ShieldTextField
import com.shieldai.android.ui.theme.ShieldAITheme
import com.kordant.android.ui.components.BadgeVariant
import com.kordant.android.ui.components.ComponentShowcase
import com.kordant.android.ui.components.InputType
import com.kordant.android.ui.components.ShieldBadge
import com.kordant.android.ui.components.ShieldButton
import com.kordant.android.ui.components.ShieldButtonSize
import com.kordant.android.ui.components.ShieldButtonVariant
import com.kordant.android.ui.components.ShieldTextField
import com.kordant.android.ui.theme.KordantTheme
import org.junit.Rule
import org.junit.Test
@@ -30,7 +30,7 @@ class ComponentTests {
@Test
fun shieldButton_rendersWithText() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Click Me", onClick = {})
}
}
@@ -41,7 +41,7 @@ class ComponentTests {
fun shieldButton_clickHandlerFires() {
var clicked = false
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Click Me", onClick = { clicked = true })
}
}
@@ -53,7 +53,7 @@ class ComponentTests {
fun shieldButton_disabledDoesNotFireClick() {
var clicked = false
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Click Me", onClick = { clicked = true }, enabled = false)
}
}
@@ -64,7 +64,7 @@ class ComponentTests {
@Test
fun shieldButton_showsLoadingIndicator() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Saving", onClick = {}, loading = true)
}
}
@@ -74,7 +74,7 @@ class ComponentTests {
@Test
fun shieldButton_variantsRender() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Primary", onClick = {}, variant = ShieldButtonVariant.Primary)
ShieldButton(text = "Secondary", onClick = {}, variant = ShieldButtonVariant.Secondary)
ShieldButton(text = "Ghost", onClick = {}, variant = ShieldButtonVariant.Ghost)
@@ -90,7 +90,7 @@ class ComponentTests {
@Test
fun shieldButton_sizesRender() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Small", onClick = {}, size = ShieldButtonSize.Small)
ShieldButton(text = "Medium", onClick = {}, size = ShieldButtonSize.Medium)
ShieldButton(text = "Large", onClick = {}, size = ShieldButtonSize.Large)
@@ -104,7 +104,7 @@ class ComponentTests {
@Test
fun shieldButton_fullWidthRenders() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldButton(text = "Full Width", onClick = {}, fullWidth = true, modifier = Modifier.fillMaxWidth())
}
}
@@ -114,7 +114,7 @@ class ComponentTests {
@Test
fun shieldTextField_rendersWithLabel() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldTextField(value = "", onValueChange = {}, label = "Email")
}
}
@@ -124,7 +124,7 @@ class ComponentTests {
@Test
fun shieldTextField_showsErrorState() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldTextField(
value = "bad",
onValueChange = {},
@@ -140,7 +140,7 @@ class ComponentTests {
@Test
fun shieldTextField_helperTextDisplayed() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldTextField(
value = "",
onValueChange = {},
@@ -155,7 +155,7 @@ class ComponentTests {
@Test
fun shieldTextField_passwordToggleExists() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldTextField(
value = "",
onValueChange = {},
@@ -170,7 +170,7 @@ class ComponentTests {
@Test
fun shieldBadge_variantsRender() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldBadge(text = "Success", variant = BadgeVariant.Success)
ShieldBadge(text = "Error", variant = BadgeVariant.Error)
ShieldBadge(text = "Warning", variant = BadgeVariant.Warning)
@@ -188,7 +188,7 @@ class ComponentTests {
@Test
fun shieldTextField_acceptsInput() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ShieldTextField(
value = "",
onValueChange = {},
@@ -202,11 +202,11 @@ class ComponentTests {
@Test
fun componentShowcase_renders() {
composeTestRule.setContent {
ShieldAITheme {
KordantTheme {
ComponentShowcase()
}
}
composeTestRule.onNodeWithText("ShieldAI Design System").assertIsDisplayed()
composeTestRule.onNodeWithText("Kordant Design System").assertIsDisplayed()
composeTestRule.onNodeWithText("ShieldButton").assertIsDisplayed()
composeTestRule.onNodeWithText("ShieldCard").assertIsDisplayed()
composeTestRule.onNodeWithText("ShieldBadge").assertIsDisplayed()

View File

@@ -1,4 +1,4 @@
package com.shieldai.android
package com.kordant.android
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
@@ -11,6 +11,6 @@ class ExampleInstrumentedTest {
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.shieldai.android", appContext.packageName)
assertEquals("com.kordant.android", appContext.packageName)
}
}

View File

@@ -5,7 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".ShieldAIApp"
android:name=".KordantApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -13,12 +13,12 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ShieldAI">
android:theme="@style/Theme.Kordant">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.ShieldAI">
android:theme="@style/Theme.Kordant">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -5,14 +5,14 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.shieldai.android.navigation.AppNavigation
import com.shieldai.android.ui.theme.ShieldAITheme
import com.shieldai.android.ui.theme.KordantTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ShieldAITheme {
KordantTheme {
AppNavigation()
}
}

View File

@@ -4,7 +4,7 @@ import android.app.Application
import com.shieldai.android.data.repository.AuthRepository
import com.shieldai.android.data.repository.AuthRepositoryImpl
class ShieldAIApp : Application() {
class KordantApp : Application() {
lateinit var authRepository: AuthRepository
private set
@@ -15,7 +15,7 @@ class ShieldAIApp : Application() {
}
companion object {
lateinit var instance: ShieldAIApp
lateinit var instance: KordantApp
private set
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,13 +11,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.shieldai.android.ShieldAIApp
import com.shieldai.android.KordantApp
import com.shieldai.android.viewmodel.AuthViewModel
@Composable
fun AppNavigation() {
val context = LocalContext.current
val app = context.applicationContext as ShieldAIApp
val app = context.applicationContext as KordantApp
val viewModel: AuthViewModel = viewModel(
factory = AuthViewModel.Factory
)

View File

@@ -0,0 +1,292 @@
package com.shieldai.android.navigation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.shieldai.android.R
import com.shieldai.android.ui.screens.auth.AuthScreen
import com.shieldai.android.ui.screens.auth.ForgotPasswordScreen
import com.shieldai.android.ui.screens.auth.ResetPasswordScreen
import com.shieldai.android.ui.screens.dashboard.AlertDetailScreen
import com.shieldai.android.ui.screens.dashboard.DashboardScreen
import com.shieldai.android.ui.screens.onboarding.OnboardingScreen
import com.shieldai.android.ui.screens.services.DarkWatchScreen
import com.shieldai.android.ui.screens.services.HomeTitleScreen
import com.shieldai.android.ui.screens.services.RemoveBrokersScreen
import com.shieldai.android.ui.screens.services.SpamShieldScreen
import com.shieldai.android.ui.screens.services.VoicePrintScreen
import com.shieldai.android.ui.screens.settings.SettingsScreen
import com.shieldai.android.viewmodel.AuthViewModel
data class ServiceNavCard(
val title: String,
val route: String,
val description: String
)
@Composable
fun NavGraph(
navController: NavHostController,
viewModel: AuthViewModel,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = Screen.Dashboard.route,
modifier = modifier
) {
composable(Screen.Dashboard.route) {
DashboardScreen(
onNavigateToAlert = { alertId ->
navController.navigate(Screen.AlertDetail.createRoute(alertId))
},
onNavigateToService = { serviceRoute ->
navController.navigate(serviceRoute)
}
)
}
composable(Screen.Alerts.route) {
AlertsScreen(
onNavigateToAlert = { alertId ->
navController.navigate(Screen.AlertDetail.createRoute(alertId))
}
)
}
composable(
route = Screen.AlertDetail.ROUTE,
arguments = listOf(navArgument("alertId") { type = NavType.StringType })
) { backStackEntry ->
val alertId = backStackEntry.arguments?.getString("alertId") ?: ""
AlertDetailScreen(
alertId = alertId,
onBack = { navController.popBackStack() }
)
}
composable(Screen.Services.route) {
ServicesHubScreen(
onNavigateToService = { route ->
navController.navigate(route)
}
)
}
composable(Screen.DarkWatch.route) {
DarkWatchScreen(
onBack = { navController.popBackStack() }
)
}
composable(Screen.VoicePrint.route) {
VoicePrintScreen(
onBack = { navController.popBackStack() }
)
}
composable(Screen.SpamShield.route) {
SpamShieldScreen(
onBack = { navController.popBackStack() }
)
}
composable(Screen.HomeTitle.route) {
HomeTitleScreen(
onBack = { navController.popBackStack() }
)
}
composable(Screen.RemoveBrokers.route) {
RemoveBrokersScreen(
onBack = { navController.popBackStack() }
)
}
composable(Screen.Settings.route) {
SettingsScreen(
onBack = { navController.popBackStack(Screen.Dashboard.route, inclusive = false) }
)
}
composable(Screen.Account.route) {
PlaceholderScreen(title = "Account")
}
composable(
route = Screen.ServiceDetail.ROUTE,
arguments = listOf(navArgument("serviceId") { type = NavType.StringType })
) { backStackEntry ->
val serviceId = backStackEntry.arguments?.getString("serviceId") ?: ""
PlaceholderScreen(title = "Service: $serviceId")
}
}
}
@Composable
fun AuthNavHost(viewModel: AuthViewModel) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Auth.route
) {
composable(Screen.Auth.route) {
AuthScreen(
viewModel = viewModel,
onNavigateToForgotPassword = {
navController.navigate(Screen.ForgotPassword.route)
},
onNavigateToResetPassword = {
navController.navigate(Screen.ResetPassword.createRoute(""))
}
)
}
composable(Screen.ForgotPassword.route) {
ForgotPasswordScreen(
viewModel = viewModel,
onBack = { navController.popBackStack() }
)
}
composable(
route = Screen.ResetPassword.route,
arguments = listOf(navArgument("email") { type = NavType.StringType; defaultValue = "" })
) { backStackEntry ->
val email = backStackEntry.arguments?.getString("email") ?: ""
ResetPasswordScreen(
viewModel = viewModel,
email = email,
onBack = { navController.popBackStack(Screen.Auth.route, false) }
)
}
}
}
@Composable
fun OnboardingNavHost(
viewModel: AuthViewModel,
onComplete: () -> Unit
) {
OnboardingScreen(
viewModel = viewModel,
onComplete = onComplete
)
}
@Composable
private fun ServicesHubScreen(
onNavigateToService: (String) -> Unit
) {
val services = listOf(
ServiceNavCard("DarkWatch", Screen.DarkWatch.route, "Monitor data exposures"),
ServiceNavCard("VoicePrint", Screen.VoicePrint.route, "Voice authentication"),
ServiceNavCard("SpamShield", Screen.SpamShield.route, "Spam call protection"),
ServiceNavCard("HomeTitle", Screen.HomeTitle.route, "Property title monitoring"),
ServiceNavCard("RemoveBrokers", Screen.RemoveBrokers.route, "Broker listing removal")
)
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Text(
text = "Services",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(services.size) { index ->
val service = services[index]
com.shieldai.android.ui.components.ShieldCard(
onClick = { onNavigateToService(service.route) },
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_services),
contentDescription = service.title,
tint = MaterialTheme.colorScheme.primary
)
Text(
text = service.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 12.dp)
)
Text(
text = service.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
}
}
}
@Composable
private fun AlertsScreen(
onNavigateToAlert: (String) -> Unit
) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Text(
text = "Alerts",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
com.shieldai.android.ui.components.ShieldEmptyState(
title = "No alerts",
description = "You have no recent alerts"
)
}
}
@Composable
private fun PlaceholderScreen(title: String) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onBackground
)
}
}

View File

@@ -18,4 +18,13 @@ sealed class Screen(val route: String) {
fun createRoute(serviceId: String) = "service_detail/$serviceId"
}
}
object AlertDetail {
const val ROUTE = "alert_detail/{alertId}"
fun createRoute(alertId: String) = "alert_detail/$alertId"
}
data object DarkWatch : Screen("darkwatch")
data object VoicePrint : Screen("voiceprint")
data object SpamShield : Screen("spamshield")
data object HomeTitle : Screen("hometitle")
data object RemoveBrokers : Screen("removebrokers")
}

View File

@@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.shieldai.android.ui.theme.ShieldAITheme
import com.shieldai.android.ui.theme.KordantTheme
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -50,7 +50,7 @@ fun ComponentShowcase(modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "ShieldAI Design System",
text = "Kordant Design System",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
@@ -263,7 +263,7 @@ private fun SectionTitle(title: String) {
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
@Composable
fun ComponentShowcasePreview() {
ShieldAITheme {
KordantTheme {
ComponentShowcase()
}
}

View File

@@ -0,0 +1,94 @@
package com.shieldai.android.ui.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
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.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.scale
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.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.toPx() / 2, size.toPx() / 2)
val radius = center.x - 16.dp.toPx()
val strokeWidth = 16.dp.toPx()
drawArc(
color = Color(0xFFE2E8F0).copy(alpha = 0.3f),
startAngle = -135f,
sweepAngle = 270f,
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2),
style = Stroke(width = strokeWidth)
)
val sweepAngle = (score.coerceIn(0, 100) / 100f) * 270f
if (sweepAngle > 0) {
drawArc(
color = startColor,
startAngle = -135f,
sweepAngle = sweepAngle,
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2),
style = Stroke(width = strokeWidth)
)
}
}
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)
)
}
}

View File

@@ -55,14 +55,14 @@ fun AuthScreen(
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "ShieldAI Logo",
contentDescription = "Kordant Logo",
modifier = Modifier.size(72.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "ShieldAI",
text = "Kordant",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary
)

View File

@@ -15,7 +15,7 @@ fun BiometricAuthScreen(
onAuthenticated: () -> Unit,
onError: (String) -> Unit,
title: String = "Biometric Authentication",
subtitle: String = "Authenticate to access ShieldAI",
subtitle: String = "Authenticate to access Kordant",
description: String = "Use your fingerprint or face to sign in"
) {
val context = LocalContext.current

View File

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

View File

@@ -0,0 +1,333 @@
package com.shieldai.android.ui.screens.dashboard
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxState
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.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.ShieldSkeletonCard
import com.shieldai.android.ui.components.ThreatGauge
import com.shieldai.android.viewmodel.DashboardViewModel
import com.shieldai.android.viewmodel.DashboardViewModel as DashboardVM
import kotlinx.coroutines.launch
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()
val scope = rememberCoroutineScope()
Box(
modifier = modifier.fillMaxSize()
) {
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 = {
ShieldButton(
text = "Retry",
onClick = { viewModel.refresh() },
variant = ShieldButtonVariant.Primary
)
}
)
} else {
ShieldEmptyState(
title = "No data",
description = "No dashboard data available"
)
}
}
else -> {
DashboardContent(
uiState = uiState,
onNavigateToAlert = onNavigateToAlert,
onNavigateToService = onNavigateToService,
onRefresh = {
scope.launch {
viewModel.refresh()
}
}
)
}
}
if (uiState.isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.TopCenter).padding(top = 16.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
}
@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() }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DashboardContent(
uiState: DashboardViewModel.DashboardUiState,
onNavigateToAlert: (String) -> Unit,
onNavigateToService: (String) -> Unit,
onRefresh: () -> Unit
) {
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
onRefresh()
true
} else {
false
}
}
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
DashboardHeader(uiState)
}
item {
ServiceSummaryRow(
uiState = uiState,
onNavigateToService = onNavigateToService
)
}
if (uiState.recentAlerts.isNotEmpty()) {
item {
Text(
text = "Recent Alerts",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
}
items(uiState.recentAlerts) { alert ->
AlertCard(alert, onNavigateToAlert)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun DashboardHeader(uiState: DashboardViewModel.DashboardUiState) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
) {
Text(
text = "Threat Overview",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 16.dp)
)
ThreatGauge(score = uiState.threatScore)
if (uiState.unreadCount > 0) {
ShieldBadge(
text = "${uiState.unreadCount} unread alert${if (uiState.unreadCount > 1) "s" else ""}",
variant = BadgeVariant.Warning,
modifier = Modifier.padding(top = 12.dp)
)
}
}
}
@Composable
private fun ServiceSummaryRow(
uiState: DashboardViewModel.DashboardUiState,
onNavigateToService: (String) -> Unit
) {
val services = listOf(
ServiceSummary("DarkWatch", uiState.watchlistCount, ImageVector.vectorResource(R.drawable.ic_services), "darkwatch"),
ServiceSummary("VoicePrint", uiState.enrollmentCount, ImageVector.vectorResource(R.drawable.ic_services), "voiceprint"),
ServiceSummary("SpamShield", uiState.spamRulesCount, ImageVector.vectorResource(R.drawable.ic_services), "spamshield"),
ServiceSummary("HomeTitle", uiState.propertiesCount, ImageVector.vectorResource(R.drawable.ic_services), "hometitle"),
ServiceSummary("RemoveBrokers", uiState.removalsCount, ImageVector.vectorResource(R.drawable.ic_services), "removebrokers")
)
Text(
text = "Services",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(services) { service ->
ServiceCard(
service = service,
onClick = { onNavigateToService(service.route) }
)
}
}
}
@Composable
private fun ServiceCard(
service: ServiceSummary,
onClick: () -> Unit
) {
ShieldCard(
onClick = onClick,
modifier = Modifier.width(130.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(8.dp)
) {
Icon(
imageVector = service.icon,
contentDescription = service.name,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = service.name,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium
)
Text(
text = "${service.count}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
}
@Composable
private fun AlertCard(
alert: Alert,
onClick: (String) -> Unit
) {
ShieldCard(
onClick = { onClick(alert.id) },
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = alert.title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = alert.message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2
)
}
AlertSeverityBadge(severity = alert.severity)
}
}
}
@Composable
private fun AlertSeverityBadge(severity: String) {
val variant = when (severity.lowercase()) {
"critical" -> BadgeVariant.Error
"high" -> BadgeVariant.Warning
"medium" -> BadgeVariant.Info
else -> BadgeVariant.Default
}
ShieldBadge(
text = severity,
variant = variant
)
}

View File

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

View File

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

View File

@@ -0,0 +1,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,
imageUrl = 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
)
}
}

View File

@@ -57,7 +57,7 @@ private val DarkColorScheme = darkColorScheme(
)
@Composable
fun ShieldAITheme(
fun KordantTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit

View File

@@ -0,0 +1,93 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.KordantApp
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(KordantApp.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 suspend fun markAlertRead(alertId: String) {
alertRepo.markRead(alertId)
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AlertDetailViewModel() as T
}
}
}
}

View File

@@ -3,7 +3,7 @@ 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.KordantApp
import com.shieldai.android.data.repository.AuthRepository
import com.shieldai.android.data.repository.AuthRepositoryImpl
import com.shieldai.android.data.repository.User
@@ -188,7 +188,7 @@ class AuthViewModel(
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val app = ShieldAIApp.instance
val app = KordantApp.instance
return AuthViewModel(app.authRepository) as T
}
}

View File

@@ -0,0 +1,100 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.KordantApp
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(KordantApp.instance)
}
init {
loadData()
}
fun refresh() {
loadData(forceRefresh = true)
}
private fun loadData(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val watchlistResult = repo.getWatchlist(forceRefresh)
val exposuresResult = repo.getExposures(forceRefresh)
val watchlist = if (watchlistResult is com.shieldai.android.data.remote.ApiResult.Success) {
watchlistResult.data
} else emptyList()
val exposures = if (exposuresResult is com.shieldai.android.data.remote.ApiResult.Success) {
exposuresResult.data
} else emptyList()
_uiState.value = _uiState.value.copy(
isLoading = false,
watchlist = watchlist,
exposures = exposures
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load data"
)
}
}
}
fun addWatchlistItem(type: String, value: String, label: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isAdding = true, error = null)
val result = repo.addWatchlistItem(type, value, label)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isAdding = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isAdding = false)
loadData(forceRefresh = true)
}
}
}
fun removeWatchlistItem(id: String) {
viewModelScope.launch {
repo.removeWatchlistItem(id)
loadData(forceRefresh = true)
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return DarkWatchViewModel() as T
}
}
}
}

View File

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

View File

@@ -0,0 +1,84 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.KordantApp
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(KordantApp.instance)
}
init {
loadProperties()
}
fun refresh() {
loadProperties(forceRefresh = true)
}
private fun loadProperties(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val result = repo.getProperties(forceRefresh)
if (result is com.shieldai.android.data.remote.ApiResult.Success) {
_uiState.value = _uiState.value.copy(
isLoading = false,
properties = result.data
)
} else {
_uiState.value = _uiState.value.copy(isLoading = false)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load properties"
)
}
}
}
fun addProperty(address: String, type: String = "residential") {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isAdding = true, error = null)
val result = repo.addProperty(address, type)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isAdding = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isAdding = false)
loadProperties(forceRefresh = true)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return HomeTitleViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.KordantApp
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(KordantApp.instance)
}
init {
loadData()
}
fun refresh() {
loadData(forceRefresh = true)
}
private fun loadData(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val listingsResult = repo.getListings(forceRefresh)
val requestsResult = repo.getRemovalRequests(forceRefresh)
val listings = if (listingsResult is com.shieldai.android.data.remote.ApiResult.Success) {
listingsResult.data
} else emptyList()
val requests = if (requestsResult is com.shieldai.android.data.remote.ApiResult.Success) {
requestsResult.data
} else emptyList()
_uiState.value = _uiState.value.copy(
isLoading = false,
listings = listings,
removalRequests = requests
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load data"
)
}
}
}
fun createRemovalRequest(listingId: String, notes: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isCreating = true, error = null)
val result = repo.createRemovalRequest(listingId, notes)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isCreating = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isCreating = false)
loadData(forceRefresh = true)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RemoveBrokersViewModel() as T
}
}
}
}

View File

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

View File

@@ -0,0 +1,98 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.KordantApp
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(KordantApp.instance)
}
init {
loadRules()
}
fun refresh() {
loadRules(forceRefresh = true)
}
private fun loadRules(forceRefresh: Boolean = false) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null)
try {
val result = repo.getRules(forceRefresh)
if (result is com.shieldai.android.data.remote.ApiResult.Success) {
val stats = repo.getStats()
_uiState.value = _uiState.value.copy(
isLoading = false,
rules = result.data,
totalBlocked = stats.totalBlocked,
totalFlagged = stats.totalFlagged,
activeRules = stats.activeRules
)
} else {
_uiState.value = _uiState.value.copy(isLoading = false)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Failed to load rules"
)
}
}
}
fun createRule(pattern: String, action: String, description: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isCreating = true, error = null)
val result = repo.createRule(pattern, action, description)
if (result is com.shieldai.android.data.remote.ApiResult.Error) {
_uiState.value = _uiState.value.copy(
isCreating = false,
error = result.message
)
} else {
_uiState.value = _uiState.value.copy(isCreating = false)
loadRules(forceRefresh = true)
}
}
}
fun toggleRule(id: String, enabled: Boolean) {
viewModelScope.launch {
repo.toggleRule(id, enabled)
loadRules(forceRefresh = true)
}
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SpamShieldViewModel() as T
}
}
}
}

View File

@@ -0,0 +1,96 @@
package com.shieldai.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.shieldai.android.KordantApp
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(KordantApp.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
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More