Compare commits
3 Commits
89822dedb8
...
3d246af3f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d246af3f7 | |||
| b62ab77fbe | |||
| c01c1a5636 |
@@ -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"
|
||||
|
||||
@@ -7,7 +7,7 @@ RESEND_API_KEY=""
|
||||
|
||||
# Docker (for deployment)
|
||||
DOCKER_TAG=latest
|
||||
GITHUB_REPOSITORY_OWNER=shieldai
|
||||
GITHUB_REPOSITORY_OWNER=kordant
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
|
||||
14
.github/workflows/deploy.yml
vendored
14
.github/workflows/deploy.yml
vendored
@@ -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 \
|
||||
|
||||
28
README.md
28
README.md
@@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
0
android/ShieldAI/.idea/.gitignore → android/Kordant/.idea/.gitignore
generated
vendored
0
android/ShieldAI/.idea/.gitignore → android/Kordant/.idea/.gitignore
generated
vendored
@@ -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 {
|
||||
@@ -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 = "9.1.1""
|
||||
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 = "1.10.1""
|
||||
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 = "1.1.5""
|
||||
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 = "3.5.1""
|
||||
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 = "2.6.1""
|
||||
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 = "1.8.0""
|
||||
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 = "2.7.7""
|
||||
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 = "2025.12.00""
|
||||
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 = "1.1.0-alpha06""
|
||||
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 = "21.0.0""
|
||||
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 = "2.9.1""
|
||||
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 = "2.9.1""
|
||||
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 = "2.2.10""
|
||||
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 = "2.2.10""
|
||||
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 = "4.12.0""
|
||||
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 = "4.12.0""
|
||||
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 = "2.10.1""
|
||||
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 = "6.4.0""
|
||||
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 = "1.7.3""
|
||||
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 = "2.11.0""
|
||||
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 = "1.7.3""
|
||||
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 = "1.4.4""
|
||||
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 = "4.12.0""
|
||||
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("biometric_enabled", 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>
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.shieldai.android.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import com.shieldai.android.data.local.CacheManager
|
||||
import com.shieldai.android.data.model.Property
|
||||
import com.shieldai.android.data.remote.ApiResult
|
||||
import com.shieldai.android.data.remote.ErrorHandler
|
||||
import com.shieldai.android.data.remote.TRPCApiService
|
||||
import com.shieldai.android.data.remote.TRPCRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
class HomeTitleRepository(
|
||||
private val api: TRPCApiService,
|
||||
private val context: Context,
|
||||
) {
|
||||
private val _properties = MutableStateFlow<List<Property>>(emptyList())
|
||||
|
||||
suspend fun getProperties(forceRefresh: Boolean = false): ApiResult<List<Property>> {
|
||||
if (!forceRefresh) {
|
||||
val cached: List<Property>? = CacheManager.load(context, "properties")
|
||||
if (cached != null) {
|
||||
_properties.value = cached
|
||||
return ApiResult.Success(cached)
|
||||
}
|
||||
}
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val response = api.propertyList(TRPCRequest.body(buildJsonObject {}))
|
||||
val properties = response.result.data
|
||||
CacheManager.save(context, "properties", properties)
|
||||
_properties.value = properties
|
||||
properties
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addProperty(address: String, type: String = "residential"): ApiResult<Property> {
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val body = buildJsonObject {
|
||||
put("address", address)
|
||||
put("type", type)
|
||||
}
|
||||
val response = api.propertyAdd(TRPCRequest.body(body))
|
||||
val property = response.result.data
|
||||
refreshCache()
|
||||
property
|
||||
}
|
||||
}
|
||||
|
||||
fun observeProperties(): Flow<List<Property>> = _properties
|
||||
|
||||
private suspend fun refreshCache() {
|
||||
ErrorHandler.executeWithRetry {
|
||||
val response = api.propertyList(TRPCRequest.body(buildJsonObject {}))
|
||||
val properties = response.result.data
|
||||
CacheManager.save(context, "properties", properties)
|
||||
_properties.value = properties
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.shieldai.android.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import com.shieldai.android.data.local.CacheManager
|
||||
import com.shieldai.android.data.model.BrokerListing
|
||||
import com.shieldai.android.data.model.RemovalRequest
|
||||
import com.shieldai.android.data.remote.ApiResult
|
||||
import com.shieldai.android.data.remote.ErrorHandler
|
||||
import com.shieldai.android.data.remote.TRPCApiService
|
||||
import com.shieldai.android.data.remote.TRPCRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
class RemoveBrokersRepository(
|
||||
private val api: TRPCApiService,
|
||||
private val context: Context,
|
||||
) {
|
||||
private val _listings = MutableStateFlow<List<BrokerListing>>(emptyList())
|
||||
private val _removalRequests = MutableStateFlow<List<RemovalRequest>>(emptyList())
|
||||
|
||||
suspend fun getListings(forceRefresh: Boolean = false): ApiResult<List<BrokerListing>> {
|
||||
if (!forceRefresh) {
|
||||
val cached: List<BrokerListing>? = CacheManager.load(context, "broker_listings")
|
||||
if (cached != null) {
|
||||
_listings.value = cached
|
||||
return ApiResult.Success(cached)
|
||||
}
|
||||
}
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val response = api.brokerListListings(TRPCRequest.body(buildJsonObject {}))
|
||||
val listings = response.result.data
|
||||
CacheManager.save(context, "broker_listings", listings)
|
||||
_listings.value = listings
|
||||
listings
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getRemovalRequests(forceRefresh: Boolean = false): ApiResult<List<RemovalRequest>> {
|
||||
if (!forceRefresh) {
|
||||
val cached: List<RemovalRequest>? = CacheManager.load(context, "removal_requests")
|
||||
if (cached != null) {
|
||||
_removalRequests.value = cached
|
||||
return ApiResult.Success(cached)
|
||||
}
|
||||
}
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val response = api.removalList(TRPCRequest.body(buildJsonObject {}))
|
||||
val requests = response.result.data
|
||||
CacheManager.save(context, "removal_requests", requests)
|
||||
_removalRequests.value = requests
|
||||
requests
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createRemovalRequest(listingId: String, notes: String? = null): ApiResult<RemovalRequest> {
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val body = buildJsonObject {
|
||||
put("listingId", listingId)
|
||||
notes?.let { put("notes", it) }
|
||||
}
|
||||
val response = api.removalCreate(TRPCRequest.body(body))
|
||||
val request = response.result.data
|
||||
refreshRemovalsCache()
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
fun observeListings(): Flow<List<BrokerListing>> = _listings
|
||||
fun observeRemovalRequests(): Flow<List<RemovalRequest>> = _removalRequests
|
||||
|
||||
private suspend fun refreshRemovalsCache() {
|
||||
ErrorHandler.executeWithRetry {
|
||||
val response = api.removalList(TRPCRequest.body(buildJsonObject {}))
|
||||
val requests = response.result.data
|
||||
CacheManager.save(context, "removal_requests", requests)
|
||||
_removalRequests.value = requests
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.shieldai.android.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import com.shieldai.android.data.local.CacheManager
|
||||
import com.shieldai.android.data.model.SpamRule
|
||||
import com.shieldai.android.data.remote.ApiResult
|
||||
import com.shieldai.android.data.remote.ErrorHandler
|
||||
import com.shieldai.android.data.remote.TRPCApiService
|
||||
import com.shieldai.android.data.remote.TRPCRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
class SpamShieldRepository(
|
||||
private val api: TRPCApiService,
|
||||
private val context: Context,
|
||||
) {
|
||||
private val _rules = MutableStateFlow<List<SpamRule>>(emptyList())
|
||||
|
||||
data class SpamStats(
|
||||
val totalBlocked: Int = 0,
|
||||
val totalFlagged: Int = 0,
|
||||
val activeRules: Int = 0
|
||||
)
|
||||
|
||||
suspend fun getRules(forceRefresh: Boolean = false): ApiResult<List<SpamRule>> {
|
||||
if (!forceRefresh) {
|
||||
val cached: List<SpamRule>? = CacheManager.load(context, "spam_rules")
|
||||
if (cached != null) {
|
||||
_rules.value = cached
|
||||
return ApiResult.Success(cached)
|
||||
}
|
||||
}
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val response = api.spamListRules(TRPCRequest.body(buildJsonObject {}))
|
||||
val rules = response.result.data
|
||||
CacheManager.save(context, "spam_rules", rules)
|
||||
_rules.value = rules
|
||||
rules
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createRule(pattern: String, action: String, description: String? = null): ApiResult<SpamRule> {
|
||||
return ErrorHandler.executeWithRetry {
|
||||
val body = buildJsonObject {
|
||||
put("pattern", pattern)
|
||||
put("action", action)
|
||||
description?.let { put("description", it) }
|
||||
}
|
||||
val response = api.spamCreateRule(TRPCRequest.body(body))
|
||||
val rule = response.result.data
|
||||
refreshCache()
|
||||
rule
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun toggleRule(id: String, enabled: Boolean): ApiResult<Unit> {
|
||||
return ErrorHandler.executeWithRetry {
|
||||
_rules.value = _rules.value.map {
|
||||
if (it.id == id) it.copy(enabled = enabled) else it
|
||||
}
|
||||
refreshCache()
|
||||
}
|
||||
}
|
||||
|
||||
fun getStats(): SpamStats {
|
||||
val rules = _rules.value
|
||||
return SpamStats(
|
||||
totalBlocked = rules.count { it.action == "block" && it.enabled },
|
||||
totalFlagged = rules.count { it.action == "flag" && it.enabled },
|
||||
activeRules = rules.count { it.enabled }
|
||||
)
|
||||
}
|
||||
|
||||
fun observeRules(): Flow<List<SpamRule>> = _rules
|
||||
|
||||
private suspend fun refreshCache() {
|
||||
ErrorHandler.executeWithRetry {
|
||||
val response = api.spamListRules(TRPCRequest.body(buildJsonObject {}))
|
||||
val rules = response.result.data
|
||||
CacheManager.save(context, "spam_rules", rules)
|
||||
_rules.value = rules
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package com.shieldai.android.di
|
||||
import android.content.Context
|
||||
import com.shieldai.android.data.repository.AlertRepository
|
||||
import com.shieldai.android.data.repository.DarkWatchRepository
|
||||
import com.shieldai.android.data.repository.HomeTitleRepository
|
||||
import com.shieldai.android.data.repository.RemoveBrokersRepository
|
||||
import com.shieldai.android.data.repository.SpamShieldRepository
|
||||
import com.shieldai.android.data.repository.SubscriptionRepository
|
||||
import com.shieldai.android.data.repository.UserRepository
|
||||
import com.shieldai.android.data.repository.VoicePrintRepository
|
||||
@@ -13,6 +16,9 @@ object RepositoryModule {
|
||||
private var voicePrintRepository: VoicePrintRepository? = null
|
||||
private var alertRepository: AlertRepository? = null
|
||||
private var subscriptionRepository: SubscriptionRepository? = null
|
||||
private var spamShieldRepository: SpamShieldRepository? = null
|
||||
private var homeTitleRepository: HomeTitleRepository? = null
|
||||
private var removeBrokersRepository: RemoveBrokersRepository? = null
|
||||
|
||||
fun provideUserRepository(context: Context): UserRepository {
|
||||
return userRepository ?: synchronized(this) {
|
||||
@@ -58,4 +64,31 @@ object RepositoryModule {
|
||||
).also { subscriptionRepository = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun provideSpamShieldRepository(context: Context): SpamShieldRepository {
|
||||
return spamShieldRepository ?: synchronized(this) {
|
||||
SpamShieldRepository(
|
||||
api = NetworkModule.provideApiService(context),
|
||||
context = context,
|
||||
).also { spamShieldRepository = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun provideHomeTitleRepository(context: Context): HomeTitleRepository {
|
||||
return homeTitleRepository ?: synchronized(this) {
|
||||
HomeTitleRepository(
|
||||
api = NetworkModule.provideApiService(context),
|
||||
context = context,
|
||||
).also { homeTitleRepository = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun provideRemoveBrokersRepository(context: Context): RemoveBrokersRepository {
|
||||
return removeBrokersRepository ?: synchronized(this) {
|
||||
RemoveBrokersRepository(
|
||||
api = NetworkModule.provideApiService(context),
|
||||
context = context,
|
||||
).also { removeBrokersRepository = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,281 @@
|
||||
package com.shieldai.android.ui.screens.dashboard
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.shieldai.android.data.model.Alert
|
||||
import com.shieldai.android.ui.components.BadgeVariant
|
||||
import com.shieldai.android.ui.components.ShieldBadge
|
||||
import com.shieldai.android.ui.components.ShieldButton
|
||||
import com.shieldai.android.ui.components.ShieldButtonVariant
|
||||
import com.shieldai.android.ui.components.ShieldCard
|
||||
import com.shieldai.android.ui.components.ShieldEmptyState
|
||||
import com.shieldai.android.ui.viewmodel.AlertDetailViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AlertDetailScreen(
|
||||
alertId: String,
|
||||
onBack: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: AlertDetailViewModel = viewModel(factory = AlertDetailViewModel.Factory)
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
if (uiState.alert == null) {
|
||||
viewModel.loadAlert(alertId)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
rememberTopAppBarState()
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = uiState.alert?.title ?: "Alert Details",
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onBack) {
|
||||
Text("Back")
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
uiState.alert == null -> {
|
||||
ShieldEmptyState(
|
||||
title = "Alert not found",
|
||||
description = "The requested alert could not be loaded",
|
||||
actionButton = {
|
||||
TextButton(onClick = onBack) {
|
||||
Text("Go Back")
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
AlertDetailContent(
|
||||
uiState = uiState,
|
||||
onMarkResolved = { viewModel.markResolved() },
|
||||
onMarkFalsePositive = { viewModel.markFalsePositive() },
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlertDetailContent(
|
||||
uiState: AlertDetailViewModel.AlertDetailUiState,
|
||||
onMarkResolved: () -> Unit,
|
||||
onMarkFalsePositive: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val alert = uiState.alert!!
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
AlertDetailHeader(alert)
|
||||
}
|
||||
|
||||
item {
|
||||
AlertDetailInfo(alert)
|
||||
}
|
||||
|
||||
if (uiState.correlatedAlerts.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Correlated Alerts (${uiState.correlatedAlerts.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
items(uiState.correlatedAlerts) { correlated ->
|
||||
CorrelatedAlertItem(correlated)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ShieldButton(
|
||||
text = "Mark Resolved",
|
||||
onClick = onMarkResolved,
|
||||
variant = ShieldButtonVariant.Primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
loading = uiState.isResolving
|
||||
)
|
||||
ShieldButton(
|
||||
text = "False Positive",
|
||||
onClick = onMarkFalsePositive,
|
||||
variant = ShieldButtonVariant.Secondary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlertDetailHeader(alert: Alert) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val variant = when (alert.severity.lowercase()) {
|
||||
"critical" -> BadgeVariant.Error
|
||||
"high" -> BadgeVariant.Warning
|
||||
"medium" -> BadgeVariant.Info
|
||||
else -> BadgeVariant.Default
|
||||
}
|
||||
ShieldBadge(
|
||||
text = "${alert.severity} severity",
|
||||
variant = variant
|
||||
)
|
||||
if (!alert.read) {
|
||||
ShieldBadge(
|
||||
text = "Unread",
|
||||
variant = BadgeVariant.Info
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = alert.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = alert.message,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlertDetailInfo(alert: Alert) {
|
||||
ShieldCard {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Details",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
InfoRow(label = "Type", value = alert.type)
|
||||
InfoRow(label = "Severity", value = alert.severity)
|
||||
InfoRow(label = "Status", value = if (alert.read) "Read" else "Unread")
|
||||
alert.date?.let {
|
||||
InfoRow(label = "Date", value = it)
|
||||
}
|
||||
alert.createdAt?.let {
|
||||
InfoRow(label = "Created", value = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoRow(label: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CorrelatedAlertItem(alert: Alert) {
|
||||
ShieldCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = alert.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
AlertSeverityBadge(severity = alert.severity)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = alert.message,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package com.shieldai.android.ui.screens.services
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.shieldai.android.R
|
||||
import com.shieldai.android.ui.components.ShieldBadge
|
||||
import com.shieldai.android.ui.components.ShieldButton
|
||||
import com.shieldai.android.ui.components.ShieldButtonVariant
|
||||
import com.shieldai.android.ui.components.ShieldCard
|
||||
import com.shieldai.android.ui.components.ShieldEmptyState
|
||||
import com.shieldai.android.ui.components.ShieldTextField
|
||||
import com.shieldai.android.viewmodel.DarkWatchViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DarkWatchScreen(
|
||||
onBack: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: DarkWatchViewModel = viewModel(factory = DarkWatchViewModel.Factory)
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var showAddSheet by remember { mutableStateOf(false) }
|
||||
var newType by remember { mutableStateOf("email") }
|
||||
var newValue by remember { mutableStateOf("") }
|
||||
var newLabel by remember { mutableStateOf("") }
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
rememberTopAppBarState()
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text("DarkWatch", fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onBack) { Text("Back") }
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showAddSheet) {
|
||||
FloatingActionButton(onClick = { showAddSheet = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dashboard),
|
||||
contentDescription = "Add to watchlist"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
when {
|
||||
uiState.isLoading && uiState.watchlist.isEmpty() -> {
|
||||
androidx.compose.foundation.layout.Box(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
uiState.watchlist.isEmpty() && uiState.exposures.isEmpty() -> {
|
||||
ShieldEmptyState(
|
||||
title = "No watchlist items",
|
||||
description = "Add people to monitor for data exposures",
|
||||
actionButton = {
|
||||
ShieldButton(
|
||||
text = "Add to Watchlist",
|
||||
onClick = { showAddSheet = true },
|
||||
variant = ShieldButtonVariant.Primary
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
DarkWatchContent(
|
||||
uiState = uiState,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddSheet) {
|
||||
AddWatchlistSheet(
|
||||
onDismiss = {
|
||||
showAddSheet = false
|
||||
newValue = ""
|
||||
newLabel = ""
|
||||
},
|
||||
onAdd = {
|
||||
viewModel.addWatchlistItem(newType, newValue, newLabel.ifBlank { null })
|
||||
showAddSheet = false
|
||||
newValue = ""
|
||||
newLabel = ""
|
||||
},
|
||||
type = newType,
|
||||
onTypeChange = { newType = it },
|
||||
value = newValue,
|
||||
onValueChange = { newValue = it },
|
||||
label = newLabel,
|
||||
onLabelChange = { newLabel = it },
|
||||
isLoading = uiState.isAdding
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DarkWatchContent(
|
||||
uiState: DarkWatchViewModel.DarkWatchUiState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (uiState.watchlist.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Watchlist (${uiState.watchlist.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
items(uiState.watchlist) { item ->
|
||||
WatchlistItemCard(item)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.exposures.isNotEmpty()) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Exposures (${uiState.exposures.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
items(uiState.exposures) { exposure ->
|
||||
ExposureCard(exposure)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchlistItemCard(item: com.shieldai.android.data.model.WatchlistItem) {
|
||||
ShieldCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.value,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (item.label != null) {
|
||||
Text(
|
||||
text = item.label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = item.type,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
ShieldBadge(
|
||||
text = item.status,
|
||||
variant = if (item.status == "active") com.shieldai.android.ui.components.BadgeVariant.Success
|
||||
else com.shieldai.android.ui.components.BadgeVariant.Default
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExposureCard(exposure: com.shieldai.android.data.model.Exposure) {
|
||||
ShieldCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = exposure.source,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
ShieldBadge(
|
||||
text = exposure.severity,
|
||||
variant = when (exposure.severity.lowercase()) {
|
||||
"critical" -> com.shieldai.android.ui.components.BadgeVariant.Error
|
||||
"high" -> com.shieldai.android.ui.components.BadgeVariant.Warning
|
||||
else -> com.shieldai.android.ui.components.BadgeVariant.Info
|
||||
}
|
||||
)
|
||||
}
|
||||
exposure.details?.let {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AddWatchlistSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
type: String,
|
||||
onTypeChange: (String) -> Unit,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
onLabelChange: (String) -> Unit,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Add to Watchlist",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
androidx.compose.material3.ExposedDropdownMenuBox(
|
||||
expanded = false,
|
||||
onExpandedChange = {}
|
||||
) {
|
||||
ShieldTextField(
|
||||
value = type,
|
||||
onValueChange = onTypeChange,
|
||||
label = "Type",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
ShieldTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = "Value (email, name, etc.)",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
ShieldTextField(
|
||||
value = label,
|
||||
onValueChange = onLabelChange,
|
||||
label = "Label (optional)",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ShieldButton(
|
||||
text = "Cancel",
|
||||
onClick = onDismiss,
|
||||
variant = ShieldButtonVariant.Secondary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
ShieldButton(
|
||||
text = "Add",
|
||||
onClick = onAdd,
|
||||
variant = ShieldButtonVariant.Primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = value.isNotBlank(),
|
||||
loading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.shieldai.android.ui.screens.services
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.shieldai.android.R
|
||||
import com.shieldai.android.ui.components.BadgeVariant
|
||||
import com.shieldai.android.ui.components.ShieldBadge
|
||||
import com.shieldai.android.ui.components.ShieldButton
|
||||
import com.shieldai.android.ui.components.ShieldButtonVariant
|
||||
import com.shieldai.android.ui.components.ShieldCard
|
||||
import com.shieldai.android.ui.components.ShieldEmptyState
|
||||
import com.shieldai.android.ui.components.ShieldTextField
|
||||
import com.shieldai.android.viewmodel.VoicePrintViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VoicePrintScreen(
|
||||
onBack: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: VoicePrintViewModel = viewModel(factory = VoicePrintViewModel.Factory)
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var showEnrollSheet by remember { mutableStateOf(false) }
|
||||
var enrollmentName by remember { mutableStateOf("") }
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
rememberTopAppBarState()
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text("VoicePrint", fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onBack) { Text("Back") }
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showEnrollSheet) {
|
||||
FloatingActionButton(onClick = { showEnrollSheet = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dashboard),
|
||||
contentDescription = "New enrollment"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
when {
|
||||
uiState.isLoading && uiState.enrollments.isEmpty() -> {
|
||||
androidx.compose.foundation.layout.Box(
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
uiState.enrollments.isEmpty() -> {
|
||||
ShieldEmptyState(
|
||||
title = "No enrollments",
|
||||
description = "Enroll voice profiles to detect impersonation",
|
||||
actionButton = {
|
||||
ShieldButton(
|
||||
text = "New Enrollment",
|
||||
onClick = { showEnrollSheet = true },
|
||||
variant = ShieldButtonVariant.Primary
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
VoicePrintContent(
|
||||
uiState = uiState,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showEnrollSheet) {
|
||||
EnrollSheet(
|
||||
onDismiss = {
|
||||
showEnrollSheet = false
|
||||
enrollmentName = ""
|
||||
},
|
||||
onEnroll = {
|
||||
viewModel.createEnrollment(enrollmentName)
|
||||
showEnrollSheet = false
|
||||
enrollmentName = ""
|
||||
},
|
||||
name = enrollmentName,
|
||||
onNameChange = { enrollmentName = it },
|
||||
isLoading = uiState.isEnrolling
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoicePrintContent(
|
||||
uiState: VoicePrintViewModel.VoicePrintUiState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "Enrollments (${uiState.enrollments.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
items(uiState.enrollments) { enrollment ->
|
||||
EnrollmentCard(enrollment)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnrollmentCard(enrollment: com.shieldai.android.data.model.VoiceEnrollment) {
|
||||
ShieldCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = enrollment.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "${enrollment.sampleCount} samples",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
ShieldBadge(
|
||||
text = enrollment.status,
|
||||
variant = when (enrollment.status.lowercase()) {
|
||||
"active" -> BadgeVariant.Success
|
||||
"pending" -> BadgeVariant.Warning
|
||||
else -> BadgeVariant.Default
|
||||
}
|
||||
)
|
||||
}
|
||||
enrollment.createdAt?.let {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Created: $it",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun EnrollSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onEnroll: () -> Unit,
|
||||
name: String,
|
||||
onNameChange: (String) -> Unit,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "New Voice Enrollment",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = "Enter a name for this voice profile. You will be able to record samples afterwards.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
ShieldTextField(
|
||||
value = name,
|
||||
onValueChange = onNameChange,
|
||||
label = "Profile name",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ShieldButton(
|
||||
text = "Cancel",
|
||||
onClick = onDismiss,
|
||||
variant = ShieldButtonVariant.Secondary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
ShieldButton(
|
||||
text = "Enroll",
|
||||
onClick = onEnroll,
|
||||
variant = ShieldButtonVariant.Primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = name.isNotBlank(),
|
||||
loading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ private val DarkColorScheme = darkColorScheme(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ShieldAITheme(
|
||||
fun KordantTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user