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"
|
REDIS_URL="redis://localhost:6379"
|
||||||
PORT=3000
|
PORT=3000
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
@@ -7,7 +7,7 @@ RESEND_API_KEY=""
|
|||||||
AWS_REGION="us-east-1"
|
AWS_REGION="us-east-1"
|
||||||
|
|
||||||
# Datadog APM Configuration
|
# Datadog APM Configuration
|
||||||
DD_SERVICE="shieldai-api"
|
DD_SERVICE="kordant-api"
|
||||||
DD_ENV="development"
|
DD_ENV="development"
|
||||||
DD_VERSION="0.1.0"
|
DD_VERSION="0.1.0"
|
||||||
DD_TRACE_ENABLED="true"
|
DD_TRACE_ENABLED="true"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ RESEND_API_KEY=""
|
|||||||
|
|
||||||
# Docker (for deployment)
|
# Docker (for deployment)
|
||||||
DOCKER_TAG=latest
|
DOCKER_TAG=latest
|
||||||
GITHUB_REPOSITORY_OWNER=shieldai
|
GITHUB_REPOSITORY_OWNER=kordant
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|||||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -58,13 +58,13 @@ jobs:
|
|||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: shieldai
|
POSTGRES_DB: kordant
|
||||||
POSTGRES_USER: shieldai
|
POSTGRES_USER: kordant
|
||||||
POSTGRES_PASSWORD: shieldai_dev
|
POSTGRES_PASSWORD: kordant_dev
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
options: >-
|
options: >-
|
||||||
--health-cmd "pg_isready -U shieldai"
|
--health-cmd "pg_isready -U kordant"
|
||||||
--health-interval 5s
|
--health-interval 5s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
@@ -92,14 +92,14 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: "postgresql://shieldai:shieldai_dev@localhost:5432/shieldai"
|
DATABASE_URL: "postgresql://kordant:kordant_dev@localhost:5432/kordant"
|
||||||
REDIS_URL: "redis://localhost:6379"
|
REDIS_URL: "redis://localhost:6379"
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
file: ./coverage/lcov.info
|
file: ./coverage/lcov.info
|
||||||
flags: unittests
|
flags: unittests
|
||||||
name: shieldai-coverage
|
name: kordant-coverage
|
||||||
fail_on_empty: false
|
fail_on_empty: false
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
tags: shieldai:${{ github.sha }}
|
tags: kordant:${{ github.sha }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
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"
|
terraform_version: "~> 1.5"
|
||||||
- name: Terraform Init
|
- name: Terraform Init
|
||||||
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
|
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
|
- name: Terraform Plan
|
||||||
id: plan
|
id: plan
|
||||||
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
|
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
|
||||||
@@ -113,8 +113,8 @@ jobs:
|
|||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository_owner }}/shieldai-${{ matrix.name }}:${{ steps.tag.outputs.tag }}
|
ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.name }}:${{ steps.tag.outputs.tag }}
|
||||||
ghcr.io/${{ github.repository_owner }}/shieldai-${{ matrix.name }}:latest
|
ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.name }}:latest
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
@@ -137,8 +137,8 @@ jobs:
|
|||||||
aws-region: us-east-1
|
aws-region: us-east-1
|
||||||
- name: Update ECS Service
|
- name: Update ECS Service
|
||||||
run: |
|
run: |
|
||||||
IMAGE="ghcr.io/${{ github.repository_owner }}/shieldai-${{ matrix.service }}:${{ needs.detect-environment.outputs.tag }}"
|
IMAGE="ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.service }}:${{ needs.detect-environment.outputs.tag }}"
|
||||||
CLUSTER="shieldai-${{ needs.detect-environment.outputs.environment }}"
|
CLUSTER="kordant-${{ needs.detect-environment.outputs.environment }}"
|
||||||
SERVICE="${{ matrix.service }}"
|
SERVICE="${{ matrix.service }}"
|
||||||
|
|
||||||
TASK_DEF=$(aws ecs describe-task-definition \
|
TASK_DEF=$(aws ecs describe-task-definition \
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
id: health
|
id: health
|
||||||
run: |
|
run: |
|
||||||
ENV="${{ needs.detect-environment.outputs.environment }}"
|
ENV="${{ needs.detect-environment.outputs.environment }}"
|
||||||
CLUSTER="shieldai-${ENV}"
|
CLUSTER="kordant-${ENV}"
|
||||||
|
|
||||||
ALB_DNS=$(aws elbv2 describe-load-balancers \
|
ALB_DNS=$(aws elbv2 describe-load-balancers \
|
||||||
--query "LoadBalancers[?contains(LoadBalancerName, '${CLUSTER}-alb')].DNSName" \
|
--query "LoadBalancers[?contains(LoadBalancerName, '${CLUSTER}-alb')].DNSName" \
|
||||||
@@ -230,7 +230,7 @@ jobs:
|
|||||||
aws-region: us-east-1
|
aws-region: us-east-1
|
||||||
- name: Rollback ECS Service
|
- name: Rollback ECS Service
|
||||||
run: |
|
run: |
|
||||||
CLUSTER="shieldai-${{ needs.detect-environment.outputs.environment }}"
|
CLUSTER="kordant-${{ needs.detect-environment.outputs.environment }}"
|
||||||
SERVICE="${{ matrix.service }}"
|
SERVICE="${{ matrix.service }}"
|
||||||
|
|
||||||
aws ecs update-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.**
|
**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.
|
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.
|
- **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.
|
- **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.
|
- **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
|
pnpm build
|
||||||
|
|
||||||
# Build individual Docker images
|
# Build individual Docker images
|
||||||
docker build -f packages/api/Dockerfile -t shieldai-api .
|
docker build -f packages/api/Dockerfile -t kordant-api .
|
||||||
docker build -f services/spamshield/Dockerfile -t shieldai-spamshield .
|
docker build -f services/spamshield/Dockerfile -t kordant-spamshield .
|
||||||
docker build -f services/darkwatch/Dockerfile -t shieldai-darkwatch .
|
docker build -f services/darkwatch/Dockerfile -t kordant-darkwatch .
|
||||||
docker build -f services/voiceprint/Dockerfile -t shieldai-voiceprint .
|
docker build -f services/voiceprint/Dockerfile -t kordant-voiceprint .
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -191,10 +191,10 @@ pnpm test
|
|||||||
pnpm test:coverage
|
pnpm test:coverage
|
||||||
|
|
||||||
# Individual service tests
|
# Individual service tests
|
||||||
pnpm test --filter @shieldai/spamshield
|
pnpm test --filter @kordant/spamshield
|
||||||
pnpm test --filter @shieldai/darkwatch
|
pnpm test --filter @kordant/darkwatch
|
||||||
pnpm test --filter @shieldai/voiceprint
|
pnpm test --filter @kordant/voiceprint
|
||||||
pnpm test --filter @shieldai/hometitle
|
pnpm test --filter @kordant/hometitle
|
||||||
|
|
||||||
# Integration & E2E
|
# Integration & E2E
|
||||||
cd packages/integration-tests && pnpm test
|
cd packages/integration-tests && pnpm test
|
||||||
@@ -242,7 +242,7 @@ See `infra/README.md` and `infra/ROLLBACK.md` for detailed operational runbooks.
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
shieldai/
|
kordant/
|
||||||
├── packages/ # Shared libraries (20 packages)
|
├── packages/ # Shared libraries (20 packages)
|
||||||
│ ├── api/ # Fastify API server
|
│ ├── api/ # Fastify API server
|
||||||
│ ├── core/ # Core shared logic
|
│ ├── core/ # Core shared logic
|
||||||
@@ -292,7 +292,7 @@ shieldai/
|
|||||||
| Push Notifications | `docs/PUSH_NOTIFICATIONS_SETUP.md` |
|
| Push Notifications | `docs/PUSH_NOTIFICATIONS_SETUP.md` |
|
||||||
| Mobile Push Integration | `docs/MOBILE_PUSH_INTEGRATION.md` |
|
| Mobile Push Integration | `docs/MOBILE_PUSH_INTEGRATION.md` |
|
||||||
| Mixpanel Analytics | `docs/MIXPANEL_ANALYTICS.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 {
|
android {
|
||||||
namespace = "com.shieldai.android"
|
namespace = "com.kordant.android"
|
||||||
compileSdk {
|
compileSdk {
|
||||||
version = release(36) {
|
version = release(36) {
|
||||||
minorApiLevel = 1
|
minorApiLevel = 1
|
||||||
@@ -13,7 +13,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.shieldai.android"
|
applicationId = "com.kordant.android"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
@@ -22,8 +22,8 @@ android {
|
|||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:3000\"")
|
buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:3000\"")
|
||||||
buildConfigField("String", "API_STAGING_URL", "\"https://staging.api.shieldai.com\"")
|
buildConfigField("String", "API_STAGING_URL", "\"https://staging.api.kordant.com\"")
|
||||||
buildConfigField("String", "API_PRODUCTION_URL", "\"https://api.shieldai.com\"")
|
buildConfigField("String", "API_PRODUCTION_URL", "\"https://api.kordant.com\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -36,7 +36,7 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
buildConfigField("String", "API_BASE_URL", "\"https://api.shieldai.com\"")
|
buildConfigField("String", "API_BASE_URL", "\"https://api.kordant.com\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
errorLine1="distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip"
|
errorLine1="distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<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"
|
line="5"
|
||||||
column="17"/>
|
column="17"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
errorLine1="agp = "9.1.1""
|
errorLine1="agp = "9.1.1""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="2"
|
line="2"
|
||||||
column="7"/>
|
column="7"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
errorLine1="coreKtx = "1.10.1""
|
errorLine1="coreKtx = "1.10.1""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="3"
|
line="3"
|
||||||
column="11"/>
|
column="11"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
errorLine1="junitVersion = "1.1.5""
|
errorLine1="junitVersion = "1.1.5""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="5"
|
line="5"
|
||||||
column="16"/>
|
column="16"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
errorLine1="espressoCore = "3.5.1""
|
errorLine1="espressoCore = "3.5.1""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="6"
|
line="6"
|
||||||
column="16"/>
|
column="16"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
errorLine1="lifecycleRuntimeKtx = "2.6.1""
|
errorLine1="lifecycleRuntimeKtx = "2.6.1""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="7"
|
line="7"
|
||||||
column="23"/>
|
column="23"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
errorLine1="activityCompose = "1.8.0""
|
errorLine1="activityCompose = "1.8.0""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="8"
|
line="8"
|
||||||
column="19"/>
|
column="19"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
errorLine1="navigationCompose = "2.7.7""
|
errorLine1="navigationCompose = "2.7.7""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="9"
|
line="9"
|
||||||
column="21"/>
|
column="21"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
errorLine1="composeBom = "2025.12.00""
|
errorLine1="composeBom = "2025.12.00""
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="11"
|
line="11"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
errorLine1="securityCrypto = "1.1.0-alpha06""
|
errorLine1="securityCrypto = "1.1.0-alpha06""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="13"
|
line="13"
|
||||||
column="18"/>
|
column="18"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
errorLine1="playServicesAuth = "21.0.0""
|
errorLine1="playServicesAuth = "21.0.0""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="15"
|
line="15"
|
||||||
column="20"/>
|
column="20"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
errorLine1="work = "2.9.1""
|
errorLine1="work = "2.9.1""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="23"
|
line="23"
|
||||||
column="8"/>
|
column="8"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
errorLine1="work = "2.9.1""
|
errorLine1="work = "2.9.1""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="23"
|
line="23"
|
||||||
column="8"/>
|
column="8"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
errorLine1="kotlin = "2.2.10""
|
errorLine1="kotlin = "2.2.10""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="10"
|
line="10"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
errorLine1="kotlin = "2.2.10""
|
errorLine1="kotlin = "2.2.10""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="10"
|
line="10"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
errorLine1="okhttp = "4.12.0""
|
errorLine1="okhttp = "4.12.0""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="16"
|
line="16"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
errorLine1="okhttp = "4.12.0""
|
errorLine1="okhttp = "4.12.0""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="16"
|
line="16"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
errorLine1="gson = "2.10.1""
|
errorLine1="gson = "2.10.1""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="17"
|
line="17"
|
||||||
column="8"/>
|
column="8"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
errorLine1="lottieCompose = "6.4.0""
|
errorLine1="lottieCompose = "6.4.0""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="18"
|
line="18"
|
||||||
column="17"/>
|
column="17"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
errorLine1="coroutinesTest = "1.7.3""
|
errorLine1="coroutinesTest = "1.7.3""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="19"
|
line="19"
|
||||||
column="18"/>
|
column="18"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
errorLine1="retrofit = "2.11.0""
|
errorLine1="retrofit = "2.11.0""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="20"
|
line="20"
|
||||||
column="12"/>
|
column="12"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
errorLine1="kotlinxSerializationJson = "1.7.3""
|
errorLine1="kotlinxSerializationJson = "1.7.3""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="22"
|
line="22"
|
||||||
column="28"/>
|
column="28"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
errorLine1="truth = "1.4.4""
|
errorLine1="truth = "1.4.4""
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="24"
|
line="24"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
errorLine1="mockwebserver = "4.12.0""
|
errorLine1="mockwebserver = "4.12.0""
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="$HOME/Code/ShieldAI/android/ShieldAI/gradle/libs.versions.toml"
|
file="$HOME/Code/Kordant/android/Kordant/gradle/libs.versions.toml"
|
||||||
line="25"
|
line="25"
|
||||||
column="17"/>
|
column="17"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -279,10 +279,10 @@
|
|||||||
<issue
|
<issue
|
||||||
id="LocalContextGetResourceValueCall"
|
id="LocalContextGetResourceValueCall"
|
||||||
message="Querying resource values using LocalContext.current"
|
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=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<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"
|
line="56"
|
||||||
column="29"/>
|
column="29"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
errorLine1=" private var userRepository: UserRepository? = null"
|
errorLine1=" private var userRepository: UserRepository? = null"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
|
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
|
||||||
line="11"
|
line="11"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
errorLine1=" private var darkWatchRepository: DarkWatchRepository? = null"
|
errorLine1=" private var darkWatchRepository: DarkWatchRepository? = null"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
|
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
|
||||||
line="12"
|
line="12"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
errorLine1=" private var voicePrintRepository: VoicePrintRepository? = null"
|
errorLine1=" private var voicePrintRepository: VoicePrintRepository? = null"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
|
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
|
||||||
line="13"
|
line="13"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
errorLine1=" private var alertRepository: AlertRepository? = null"
|
errorLine1=" private var alertRepository: AlertRepository? = null"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
|
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
|
||||||
line="14"
|
line="14"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
errorLine1=" private var subscriptionRepository: SubscriptionRepository? = null"
|
errorLine1=" private var subscriptionRepository: SubscriptionRepository? = null"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/shieldai/android/di/RepositoryModule.kt"
|
file="src/main/java/com/kordant/android/di/RepositoryModule.kt"
|
||||||
line="15"
|
line="15"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -480,7 +480,7 @@
|
|||||||
errorLine1=" securePrefs.edit()"
|
errorLine1=" securePrefs.edit()"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<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"
|
line="144"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -491,7 +491,7 @@
|
|||||||
errorLine1=" securePrefs.edit()"
|
errorLine1=" securePrefs.edit()"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<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"
|
line="155"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -502,7 +502,7 @@
|
|||||||
errorLine1=" prefs.edit().putBoolean("biometric_enabled", enabled).apply()"
|
errorLine1=" prefs.edit().putBoolean("biometric_enabled", enabled).apply()"
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~">
|
||||||
<location
|
<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"
|
line="88"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.shieldai.android
|
package com.kordant.android
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.ui.Modifier
|
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.performClick
|
||||||
import androidx.compose.ui.test.performTextClearance
|
import androidx.compose.ui.test.performTextClearance
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import com.shieldai.android.ui.components.BadgeVariant
|
import com.kordant.android.ui.components.BadgeVariant
|
||||||
import com.shieldai.android.ui.components.ComponentShowcase
|
import com.kordant.android.ui.components.ComponentShowcase
|
||||||
import com.shieldai.android.ui.components.InputType
|
import com.kordant.android.ui.components.InputType
|
||||||
import com.shieldai.android.ui.components.ShieldBadge
|
import com.kordant.android.ui.components.ShieldBadge
|
||||||
import com.shieldai.android.ui.components.ShieldButton
|
import com.kordant.android.ui.components.ShieldButton
|
||||||
import com.shieldai.android.ui.components.ShieldButtonSize
|
import com.kordant.android.ui.components.ShieldButtonSize
|
||||||
import com.shieldai.android.ui.components.ShieldButtonVariant
|
import com.kordant.android.ui.components.ShieldButtonVariant
|
||||||
import com.shieldai.android.ui.components.ShieldTextField
|
import com.kordant.android.ui.components.ShieldTextField
|
||||||
import com.shieldai.android.ui.theme.ShieldAITheme
|
import com.kordant.android.ui.theme.KordantTheme
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldButton_rendersWithText() {
|
fun shieldButton_rendersWithText() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Click Me", onClick = {})
|
ShieldButton(text = "Click Me", onClick = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ class ComponentTests {
|
|||||||
fun shieldButton_clickHandlerFires() {
|
fun shieldButton_clickHandlerFires() {
|
||||||
var clicked = false
|
var clicked = false
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Click Me", onClick = { clicked = true })
|
ShieldButton(text = "Click Me", onClick = { clicked = true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ class ComponentTests {
|
|||||||
fun shieldButton_disabledDoesNotFireClick() {
|
fun shieldButton_disabledDoesNotFireClick() {
|
||||||
var clicked = false
|
var clicked = false
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Click Me", onClick = { clicked = true }, enabled = false)
|
ShieldButton(text = "Click Me", onClick = { clicked = true }, enabled = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldButton_showsLoadingIndicator() {
|
fun shieldButton_showsLoadingIndicator() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Saving", onClick = {}, loading = true)
|
ShieldButton(text = "Saving", onClick = {}, loading = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldButton_variantsRender() {
|
fun shieldButton_variantsRender() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Primary", onClick = {}, variant = ShieldButtonVariant.Primary)
|
ShieldButton(text = "Primary", onClick = {}, variant = ShieldButtonVariant.Primary)
|
||||||
ShieldButton(text = "Secondary", onClick = {}, variant = ShieldButtonVariant.Secondary)
|
ShieldButton(text = "Secondary", onClick = {}, variant = ShieldButtonVariant.Secondary)
|
||||||
ShieldButton(text = "Ghost", onClick = {}, variant = ShieldButtonVariant.Ghost)
|
ShieldButton(text = "Ghost", onClick = {}, variant = ShieldButtonVariant.Ghost)
|
||||||
@@ -90,7 +90,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldButton_sizesRender() {
|
fun shieldButton_sizesRender() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Small", onClick = {}, size = ShieldButtonSize.Small)
|
ShieldButton(text = "Small", onClick = {}, size = ShieldButtonSize.Small)
|
||||||
ShieldButton(text = "Medium", onClick = {}, size = ShieldButtonSize.Medium)
|
ShieldButton(text = "Medium", onClick = {}, size = ShieldButtonSize.Medium)
|
||||||
ShieldButton(text = "Large", onClick = {}, size = ShieldButtonSize.Large)
|
ShieldButton(text = "Large", onClick = {}, size = ShieldButtonSize.Large)
|
||||||
@@ -104,7 +104,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldButton_fullWidthRenders() {
|
fun shieldButton_fullWidthRenders() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldButton(text = "Full Width", onClick = {}, fullWidth = true, modifier = Modifier.fillMaxWidth())
|
ShieldButton(text = "Full Width", onClick = {}, fullWidth = true, modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldTextField_rendersWithLabel() {
|
fun shieldTextField_rendersWithLabel() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldTextField(value = "", onValueChange = {}, label = "Email")
|
ShieldTextField(value = "", onValueChange = {}, label = "Email")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldTextField_showsErrorState() {
|
fun shieldTextField_showsErrorState() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldTextField(
|
ShieldTextField(
|
||||||
value = "bad",
|
value = "bad",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -140,7 +140,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldTextField_helperTextDisplayed() {
|
fun shieldTextField_helperTextDisplayed() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldTextField(
|
ShieldTextField(
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -155,7 +155,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldTextField_passwordToggleExists() {
|
fun shieldTextField_passwordToggleExists() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldTextField(
|
ShieldTextField(
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -170,7 +170,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldBadge_variantsRender() {
|
fun shieldBadge_variantsRender() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldBadge(text = "Success", variant = BadgeVariant.Success)
|
ShieldBadge(text = "Success", variant = BadgeVariant.Success)
|
||||||
ShieldBadge(text = "Error", variant = BadgeVariant.Error)
|
ShieldBadge(text = "Error", variant = BadgeVariant.Error)
|
||||||
ShieldBadge(text = "Warning", variant = BadgeVariant.Warning)
|
ShieldBadge(text = "Warning", variant = BadgeVariant.Warning)
|
||||||
@@ -188,7 +188,7 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun shieldTextField_acceptsInput() {
|
fun shieldTextField_acceptsInput() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ShieldTextField(
|
ShieldTextField(
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -202,11 +202,11 @@ class ComponentTests {
|
|||||||
@Test
|
@Test
|
||||||
fun componentShowcase_renders() {
|
fun componentShowcase_renders() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ComponentShowcase()
|
ComponentShowcase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composeTestRule.onNodeWithText("ShieldAI Design System").assertIsDisplayed()
|
composeTestRule.onNodeWithText("Kordant Design System").assertIsDisplayed()
|
||||||
composeTestRule.onNodeWithText("ShieldButton").assertIsDisplayed()
|
composeTestRule.onNodeWithText("ShieldButton").assertIsDisplayed()
|
||||||
composeTestRule.onNodeWithText("ShieldCard").assertIsDisplayed()
|
composeTestRule.onNodeWithText("ShieldCard").assertIsDisplayed()
|
||||||
composeTestRule.onNodeWithText("ShieldBadge").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.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
@@ -11,6 +11,6 @@ class ExampleInstrumentedTest {
|
|||||||
@Test
|
@Test
|
||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
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" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ShieldAIApp"
|
android:name=".KordantApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -13,12 +13,12 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ShieldAI">
|
android:theme="@style/Theme.Kordant">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.ShieldAI">
|
android:theme="@style/Theme.Kordant">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -5,14 +5,14 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import com.shieldai.android.navigation.AppNavigation
|
import com.shieldai.android.navigation.AppNavigation
|
||||||
import com.shieldai.android.ui.theme.ShieldAITheme
|
import com.shieldai.android.ui.theme.KordantTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
AppNavigation()
|
AppNavigation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import android.app.Application
|
|||||||
import com.shieldai.android.data.repository.AuthRepository
|
import com.shieldai.android.data.repository.AuthRepository
|
||||||
import com.shieldai.android.data.repository.AuthRepositoryImpl
|
import com.shieldai.android.data.repository.AuthRepositoryImpl
|
||||||
|
|
||||||
class ShieldAIApp : Application() {
|
class KordantApp : Application() {
|
||||||
lateinit var authRepository: AuthRepository
|
lateinit var authRepository: AuthRepository
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class ShieldAIApp : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: ShieldAIApp
|
lateinit var instance: KordantApp
|
||||||
private set
|
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 android.content.Context
|
||||||
import com.shieldai.android.data.repository.AlertRepository
|
import com.shieldai.android.data.repository.AlertRepository
|
||||||
import com.shieldai.android.data.repository.DarkWatchRepository
|
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.SubscriptionRepository
|
||||||
import com.shieldai.android.data.repository.UserRepository
|
import com.shieldai.android.data.repository.UserRepository
|
||||||
import com.shieldai.android.data.repository.VoicePrintRepository
|
import com.shieldai.android.data.repository.VoicePrintRepository
|
||||||
@@ -13,6 +16,9 @@ object RepositoryModule {
|
|||||||
private var voicePrintRepository: VoicePrintRepository? = null
|
private var voicePrintRepository: VoicePrintRepository? = null
|
||||||
private var alertRepository: AlertRepository? = null
|
private var alertRepository: AlertRepository? = null
|
||||||
private var subscriptionRepository: SubscriptionRepository? = 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 {
|
fun provideUserRepository(context: Context): UserRepository {
|
||||||
return userRepository ?: synchronized(this) {
|
return userRepository ?: synchronized(this) {
|
||||||
@@ -58,4 +64,31 @@ object RepositoryModule {
|
|||||||
).also { subscriptionRepository = it }
|
).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.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.shieldai.android.ShieldAIApp
|
import com.shieldai.android.KordantApp
|
||||||
import com.shieldai.android.viewmodel.AuthViewModel
|
import com.shieldai.android.viewmodel.AuthViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation() {
|
fun AppNavigation() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val app = context.applicationContext as ShieldAIApp
|
val app = context.applicationContext as KordantApp
|
||||||
val viewModel: AuthViewModel = viewModel(
|
val viewModel: AuthViewModel = viewModel(
|
||||||
factory = AuthViewModel.Factory
|
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"
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.shieldai.android.ui.theme.ShieldAITheme
|
import com.shieldai.android.ui.theme.KordantTheme
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -50,7 +50,7 @@ fun ComponentShowcase(modifier: Modifier = Modifier) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "ShieldAI Design System",
|
text = "Kordant Design System",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
@@ -263,7 +263,7 @@ private fun SectionTitle(title: String) {
|
|||||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
|
||||||
@Composable
|
@Composable
|
||||||
fun ComponentShowcasePreview() {
|
fun ComponentShowcasePreview() {
|
||||||
ShieldAITheme {
|
KordantTheme {
|
||||||
ComponentShowcase()
|
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(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||||
contentDescription = "ShieldAI Logo",
|
contentDescription = "Kordant Logo",
|
||||||
modifier = Modifier.size(72.dp)
|
modifier = Modifier.size(72.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "ShieldAI",
|
text = "Kordant",
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
@@ -15,7 +15,7 @@ fun BiometricAuthScreen(
|
|||||||
onAuthenticated: () -> Unit,
|
onAuthenticated: () -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
title: String = "Biometric Authentication",
|
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"
|
description: String = "Use your fingerprint or face to sign in"
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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
|
@Composable
|
||||||
fun ShieldAITheme(
|
fun KordantTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
dynamicColor: Boolean = false,
|
dynamicColor: Boolean = false,
|
||||||
content: @Composable () -> Unit
|
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.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
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.AuthRepository
|
||||||
import com.shieldai.android.data.repository.AuthRepositoryImpl
|
import com.shieldai.android.data.repository.AuthRepositoryImpl
|
||||||
import com.shieldai.android.data.repository.User
|
import com.shieldai.android.data.repository.User
|
||||||
@@ -188,7 +188,7 @@ class AuthViewModel(
|
|||||||
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
|
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
val app = ShieldAIApp.instance
|
val app = KordantApp.instance
|
||||||
return AuthViewModel(app.authRepository) as T
|
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