fixing a ton of sloppy mess...
This commit is contained in:
parent
f970f1bac8
commit
d2608e0188
File diff suppressed because it is too large
Load Diff
39
ooad-docs/BENCHMARK_EVIDENCE_TEMPLATE.md
Normal file
39
ooad-docs/BENCHMARK_EVIDENCE_TEMPLATE.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Benchmark Evidence Template
|
||||||
|
|
||||||
|
This file is intentionally a template, not a fabricated result. Fill it after running benchmarks on the required machine/device.
|
||||||
|
|
||||||
|
## Flutter Device Benchmark
|
||||||
|
|
||||||
|
| Metric | Tool | Threshold | Result | Evidence File |
|
||||||
|
|---|---|---:|---:|---|
|
||||||
|
| Memory baseline | DevTools Memory | report MB | TBD | screenshot TBD |
|
||||||
|
| Memory leak check | DevTools Memory | no steady growth | TBD | screenshot TBD |
|
||||||
|
| Frame rate / jank | DevTools Performance | >= 90% frames < 16ms | TBD | screenshot TBD |
|
||||||
|
| CPU profile | DevTools CPU Profiler | top 3 operations documented | TBD | screenshot TBD |
|
||||||
|
| API latency | Dio logs | < 1500ms | TBD | log TBD |
|
||||||
|
| Cold start | `flutter run --profile --trace-startup` | < 3000ms | TBD | trace TBD |
|
||||||
|
| APK size | `flutter build apk --analyze-size` | < 50MB | TBD | report TBD |
|
||||||
|
|
||||||
|
## Backend Load Benchmark
|
||||||
|
|
||||||
|
| Metric | Tool | Threshold | Result | Evidence File |
|
||||||
|
|---|---|---:|---:|---|
|
||||||
|
| Throughput | k6/JMeter | >= 100 req/s | TBD | k6 summary TBD |
|
||||||
|
| p95 latency | k6/JMeter | < 500ms | TBD | k6 summary TBD |
|
||||||
|
| Error rate | k6/JMeter | < 1% | TBD | k6 summary TBD |
|
||||||
|
| Slow query | DB/Actuator logs | no query > 200ms | TBD | log TBD |
|
||||||
|
| JVM memory | Actuator metrics | no heap exhaustion | TBD | metrics TBD |
|
||||||
|
|
||||||
|
## Commands To Run
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd "D:\CodeSpace\Final Project Gabungan - Broken Test\walkguide-mobile\walkguide_app"
|
||||||
|
flutter run --profile --trace-startup
|
||||||
|
flutter build apk --analyze-size
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "walkguide-backend/demo/k6-tests"
|
||||||
|
./run-all-tests.sh smoke
|
||||||
|
./run-all-tests.sh load
|
||||||
|
```
|
||||||
38
ooad-docs/DESIGN_PATTERNS.md
Normal file
38
ooad-docs/DESIGN_PATTERNS.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# WalkGuide Design Pattern Documentation
|
||||||
|
|
||||||
|
## Builder - Creational
|
||||||
|
- Backend DTO/entity builders via Lombok `@Builder`.
|
||||||
|
- Files: `PairingStatusResponse.java`, `AgoraTokenResponse.java`, entity classes such as `PairingRelation.java`.
|
||||||
|
- Reason: response/entity construction has many optional fields, so builder calls are clearer than long constructors.
|
||||||
|
|
||||||
|
## Singleton / Service Locator - Creational
|
||||||
|
- Flutter registers app-wide services once with GetIt.
|
||||||
|
- File: `walkguide-mobile/walkguide_app/lib/app/injection_container.dart`.
|
||||||
|
- Examples: `TtsService`, `WebSocketService`, `CallService`, `YoloDetector`.
|
||||||
|
- Reason: resource-heavy services need one shared lifecycle.
|
||||||
|
|
||||||
|
## Facade - Structural
|
||||||
|
- Flutter service classes hide low-level plugins from UI.
|
||||||
|
- Files: `TtsService`, `HapticService`, `FcmService`, `CallService`, `WebSocketService`.
|
||||||
|
- Reason: screens call simple domain-oriented methods instead of plugin APIs directly.
|
||||||
|
|
||||||
|
## Repository - Structural
|
||||||
|
- Backend repositories abstract JPA persistence.
|
||||||
|
- Files: `UserRepository`, `PairingRelationRepository`, `LocationHistoryRepository`, etc.
|
||||||
|
- Reason: service layer works against repository contracts, not SQL.
|
||||||
|
|
||||||
|
## Observer - Behavioral
|
||||||
|
- Flutter BLoC/Cubit and ChangeNotifier notify screens on state changes.
|
||||||
|
- Backend WebSocket broker pushes location/SOS/notification updates to subscribers.
|
||||||
|
- Files: `AppCubit`, `WebSocketService`, `LocationBroadcaster`.
|
||||||
|
- Reason: real-time features need push updates without polling.
|
||||||
|
|
||||||
|
## Strategy - Behavioral
|
||||||
|
- Obstacle direction/distance analysis can vary independently from camera capture.
|
||||||
|
- File: `core/ai/obstacle_analyzer.dart`.
|
||||||
|
- Reason: detection interpretation is isolated so thresholds/rules can evolve.
|
||||||
|
|
||||||
|
## Chain of Responsibility - Behavioral
|
||||||
|
- Dio interceptors handle token injection, refresh, and error flow.
|
||||||
|
- File: `core/network/api_client.dart`.
|
||||||
|
- Reason: each request passes through consistent auth/error handling before reaching UI.
|
||||||
29
ooad-docs/TRACEABILITY_AUDIT.md
Normal file
29
ooad-docs/TRACEABILITY_AUDIT.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# WalkGuide Traceability Audit
|
||||||
|
|
||||||
|
## Use Case To Code
|
||||||
|
|
||||||
|
| Use Case | Flutter Entry | Backend Entry | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Register/Login | `features/auth/*` | `AuthController`, `AuthService` | Implemented |
|
||||||
|
| Pair Guardian/User | `features/pairing/pairing_screens.dart` | `PairingController`, `PairingService` | Implemented |
|
||||||
|
| Start/Stop WalkGuide | `features/walk_guide/walk_guide_screen.dart` | `POST /user/walkguide/start`, `POST /user/walkguide/stop` | Implemented |
|
||||||
|
| Obstacle Detection | `core/ai/*`, `walk_guide_screen.dart` | `POST /user/obstacle` | Partially implemented; real `.tflite` model file still required |
|
||||||
|
| Location Tracking | `LocationReporterService`, navigation/walk screens | `LocationService`, `LocationBroadcaster` | Implemented |
|
||||||
|
| SOS | `features/sos/sos_screen.dart` | `SosService`, `GuardianController` | Implemented |
|
||||||
|
| Notifications | `features/notifications/notification_screen.dart` | `NotificationService`, `FcmService` | Partially implemented; backend FCM is log-only without credentials |
|
||||||
|
| Guardian Dashboard | `guardian_dashboard_screen.dart` | `GuardianDashboardService` | Implemented |
|
||||||
|
| Call | `features/call/call_screen.dart`, `CallService` | `CallController`, `AgoraTokenService` | Partially implemented; Agora App ID/credential required for live call |
|
||||||
|
| Geofence | guardian dashboard screens | `GeofenceService` | Implemented |
|
||||||
|
|
||||||
|
## Design Deviations
|
||||||
|
|
||||||
|
- Flutter uses a pragmatic mix of BLoC/Cubit, ChangeNotifier, and StatefulWidget. This deviates from strict full-BLoC architecture but keeps feature screens small for the exam demo.
|
||||||
|
- Offline storage uses `SharedPreferences` queue instead of a full Drift database for cached entities. The dependency exists, but production-grade SQLite cache is not fully wired.
|
||||||
|
- Backend FCM service is log-only until Firebase Admin credentials are provided.
|
||||||
|
- Agora call flow is implemented at API and service level, but live RTC depends on a real Agora App ID.
|
||||||
|
|
||||||
|
## Remaining Evidence
|
||||||
|
|
||||||
|
- Export rendered OOAD diagrams from the PlantUML files in this folder.
|
||||||
|
- Run backend tests and archive JaCoCo/Surefire reports.
|
||||||
|
- Run Flutter benchmarks on a physical Android device and add screenshots/results.
|
||||||
71
ooad-docs/diagrams/class-diagram.puml
Normal file
71
ooad-docs/diagrams/class-diagram.puml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
@startuml
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
|
||||||
|
class User {
|
||||||
|
+Long id
|
||||||
|
+String email
|
||||||
|
+String role
|
||||||
|
+String uniqueUserId
|
||||||
|
+String displayName
|
||||||
|
+String fcmToken
|
||||||
|
}
|
||||||
|
|
||||||
|
class PairingRelation {
|
||||||
|
+Long id
|
||||||
|
+PairingStatus status
|
||||||
|
+LocalDateTime invitedAt
|
||||||
|
+LocalDateTime respondedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationHistory
|
||||||
|
class ActivityLog
|
||||||
|
class ObstacleLog
|
||||||
|
class GuardianNotification
|
||||||
|
class SosEvent
|
||||||
|
class UserSettings
|
||||||
|
class AiConfig
|
||||||
|
class VoiceCommandConfig
|
||||||
|
class HardwareShortcut
|
||||||
|
class GeofenceConfig
|
||||||
|
class RefreshToken
|
||||||
|
|
||||||
|
User "1" -- "0..*" LocationHistory
|
||||||
|
User "1" -- "0..*" ActivityLog
|
||||||
|
User "1" -- "0..*" ObstacleLog
|
||||||
|
User "1" -- "0..*" SosEvent
|
||||||
|
User "1" -- "0..1" UserSettings
|
||||||
|
User "1" -- "0..*" RefreshToken
|
||||||
|
User "1" -- "0..*" PairingRelation : guardian
|
||||||
|
User "1" -- "0..*" PairingRelation : user
|
||||||
|
PairingRelation "1" -- "0..*" GuardianNotification
|
||||||
|
PairingRelation "1" -- "0..1" AiConfig
|
||||||
|
PairingRelation "1" -- "0..*" VoiceCommandConfig
|
||||||
|
PairingRelation "1" -- "0..*" HardwareShortcut
|
||||||
|
PairingRelation "1" -- "0..1" GeofenceConfig
|
||||||
|
|
||||||
|
class AuthController
|
||||||
|
class PairingController
|
||||||
|
class UserController
|
||||||
|
class GuardianController
|
||||||
|
class CallController
|
||||||
|
class AuthService
|
||||||
|
class PairingService
|
||||||
|
class LocationService
|
||||||
|
class NotificationService
|
||||||
|
class SosService
|
||||||
|
class AgoraTokenService
|
||||||
|
class FcmService
|
||||||
|
class LocationBroadcaster
|
||||||
|
|
||||||
|
AuthController --> AuthService
|
||||||
|
PairingController --> PairingService
|
||||||
|
UserController --> LocationService
|
||||||
|
UserController --> SosService
|
||||||
|
GuardianController --> NotificationService
|
||||||
|
GuardianController --> SosService
|
||||||
|
CallController --> AgoraTokenService
|
||||||
|
CallController --> FcmService
|
||||||
|
LocationService --> LocationBroadcaster
|
||||||
|
NotificationService --> LocationBroadcaster
|
||||||
|
SosService --> LocationBroadcaster
|
||||||
|
@enduml
|
||||||
35
ooad-docs/diagrams/component-diagram.puml
Normal file
35
ooad-docs/diagrams/component-diagram.puml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@startuml
|
||||||
|
skinparam componentStyle rectangle
|
||||||
|
|
||||||
|
component "Flutter Mobile App" {
|
||||||
|
[Presentation Screens]
|
||||||
|
[ApiClient + Interceptors]
|
||||||
|
[TTS/STT/YOLO/Location Services]
|
||||||
|
[WebSocketService]
|
||||||
|
[CallService]
|
||||||
|
}
|
||||||
|
|
||||||
|
component "Spring Boot Backend" {
|
||||||
|
[Controllers]
|
||||||
|
[Services]
|
||||||
|
[Repositories]
|
||||||
|
[Security/JWT]
|
||||||
|
[WebSocket Broker]
|
||||||
|
}
|
||||||
|
|
||||||
|
database "PostgreSQL" as DB
|
||||||
|
cloud "Firebase FCM" as FCM
|
||||||
|
cloud "Agora RTC" as Agora
|
||||||
|
cloud "OpenStreetMap/OSRM" as Maps
|
||||||
|
|
||||||
|
[Presentation Screens] --> [ApiClient + Interceptors]
|
||||||
|
[ApiClient + Interceptors] --> [Controllers] : REST /api/v1
|
||||||
|
[WebSocketService] --> [WebSocket Broker] : STOMP /ws
|
||||||
|
[CallService] --> [Controllers] : /shared/call
|
||||||
|
[Controllers] --> [Services]
|
||||||
|
[Services] --> [Repositories]
|
||||||
|
[Repositories] --> DB
|
||||||
|
[Services] --> FCM
|
||||||
|
[CallService] --> Agora
|
||||||
|
[Presentation Screens] --> Maps
|
||||||
|
@enduml
|
||||||
101
ooad-docs/diagrams/erd-diagram.puml
Normal file
101
ooad-docs/diagrams/erd-diagram.puml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
@startuml
|
||||||
|
hide circle
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
entity users {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
email : VARCHAR
|
||||||
|
password : VARCHAR
|
||||||
|
role : VARCHAR
|
||||||
|
unique_user_id : CHAR(12)
|
||||||
|
display_name : VARCHAR
|
||||||
|
fcm_token : VARCHAR
|
||||||
|
created_at : TIMESTAMP
|
||||||
|
}
|
||||||
|
|
||||||
|
entity pairing_relations {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
guardian_id : BIGINT <<FK>>
|
||||||
|
user_id : BIGINT <<FK>>
|
||||||
|
status : VARCHAR
|
||||||
|
invited_at : TIMESTAMP
|
||||||
|
responded_at : TIMESTAMP
|
||||||
|
}
|
||||||
|
|
||||||
|
entity activity_logs {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
user_id : BIGINT <<FK>>
|
||||||
|
log_type : VARCHAR
|
||||||
|
description : TEXT
|
||||||
|
metadata : JSONB
|
||||||
|
created_at : TIMESTAMP
|
||||||
|
}
|
||||||
|
|
||||||
|
entity obstacle_logs {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
user_id : BIGINT <<FK>>
|
||||||
|
label : VARCHAR
|
||||||
|
confidence : FLOAT
|
||||||
|
direction : VARCHAR
|
||||||
|
estimated_dist : VARCHAR
|
||||||
|
lat : DOUBLE
|
||||||
|
lng : DOUBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
entity location_history {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
user_id : BIGINT <<FK>>
|
||||||
|
lat : DOUBLE
|
||||||
|
lng : DOUBLE
|
||||||
|
accuracy : FLOAT
|
||||||
|
speed : FLOAT
|
||||||
|
heading : FLOAT
|
||||||
|
}
|
||||||
|
|
||||||
|
entity guardian_notifications {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
guardian_id : BIGINT <<FK>>
|
||||||
|
user_id : BIGINT <<FK>>
|
||||||
|
notif_type : VARCHAR
|
||||||
|
content : TEXT
|
||||||
|
is_read : BOOLEAN
|
||||||
|
}
|
||||||
|
|
||||||
|
entity sos_events {
|
||||||
|
* id : BIGSERIAL <<PK>>
|
||||||
|
--
|
||||||
|
user_id : BIGINT <<FK>>
|
||||||
|
trigger_type : VARCHAR
|
||||||
|
lat : DOUBLE
|
||||||
|
lng : DOUBLE
|
||||||
|
status : VARCHAR
|
||||||
|
}
|
||||||
|
|
||||||
|
entity user_settings
|
||||||
|
entity ai_configs
|
||||||
|
entity voice_command_configs
|
||||||
|
entity hardware_shortcuts
|
||||||
|
entity geofence_configs
|
||||||
|
entity refresh_tokens
|
||||||
|
|
||||||
|
users ||--o{ pairing_relations : guardian_id
|
||||||
|
users ||--o{ pairing_relations : user_id
|
||||||
|
users ||--o{ activity_logs
|
||||||
|
users ||--o{ obstacle_logs
|
||||||
|
users ||--o{ location_history
|
||||||
|
users ||--o{ guardian_notifications : guardian_id
|
||||||
|
users ||--o{ guardian_notifications : user_id
|
||||||
|
users ||--o{ sos_events
|
||||||
|
users ||--o| user_settings
|
||||||
|
users ||--o{ refresh_tokens
|
||||||
|
pairing_relations ||--o| ai_configs
|
||||||
|
pairing_relations ||--o{ voice_command_configs
|
||||||
|
pairing_relations ||--o{ hardware_shortcuts
|
||||||
|
pairing_relations ||--o| geofence_configs
|
||||||
|
@enduml
|
||||||
19
ooad-docs/diagrams/sequence-login.puml
Normal file
19
ooad-docs/diagrams/sequence-login.puml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@startuml
|
||||||
|
actor User
|
||||||
|
participant "Flutter LoginScreen" as UI
|
||||||
|
participant "ApiClient/Dio" as Dio
|
||||||
|
participant "AuthController" as Controller
|
||||||
|
participant "AuthService" as Service
|
||||||
|
database "PostgreSQL" as DB
|
||||||
|
|
||||||
|
User -> UI : enter email/password
|
||||||
|
UI -> Dio : POST /api/v1/auth/login
|
||||||
|
Dio -> Controller : LoginRequest
|
||||||
|
Controller -> Service : login(req)
|
||||||
|
Service -> DB : find user + refresh token
|
||||||
|
DB --> Service : user
|
||||||
|
Service --> Controller : AuthDataResponse
|
||||||
|
Controller --> Dio : ApiResponse<AuthDataResponse>
|
||||||
|
Dio --> UI : tokens + role
|
||||||
|
UI -> UI : save tokens and route by role
|
||||||
|
@enduml
|
||||||
23
ooad-docs/diagrams/sequence-pairing.puml
Normal file
23
ooad-docs/diagrams/sequence-pairing.puml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@startuml
|
||||||
|
actor Guardian
|
||||||
|
actor User
|
||||||
|
participant "GuardianPairingScreen" as GUI
|
||||||
|
participant "PairingController" as Controller
|
||||||
|
participant "PairingService" as Service
|
||||||
|
database "PostgreSQL" as DB
|
||||||
|
participant "FcmService" as FCM
|
||||||
|
|
||||||
|
Guardian -> GUI : input uniqueUserId
|
||||||
|
GUI -> Controller : POST /shared/pairing/invite
|
||||||
|
Controller -> Service : inviteUser(guardianId, uniqueUserId)
|
||||||
|
Service -> DB : validate and create PENDING relation
|
||||||
|
Service -> FCM : notify user
|
||||||
|
Service --> Controller : PairingStatusResponse
|
||||||
|
Controller --> GUI : invite status
|
||||||
|
|
||||||
|
User -> Controller : POST /shared/pairing/respond
|
||||||
|
Controller -> Service : respondToPairing(userId, pairingId, accept)
|
||||||
|
Service -> DB : update ACTIVE/REJECTED
|
||||||
|
Service -> DB : seed default AI/voice/shortcut configs
|
||||||
|
Service -> FCM : notify guardian
|
||||||
|
@enduml
|
||||||
22
ooad-docs/diagrams/sequence-sos.puml
Normal file
22
ooad-docs/diagrams/sequence-sos.puml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@startuml
|
||||||
|
actor User
|
||||||
|
actor Guardian
|
||||||
|
participant "SosScreen" as UI
|
||||||
|
participant "UserController" as UserController
|
||||||
|
participant "SosService" as SosService
|
||||||
|
database "PostgreSQL" as DB
|
||||||
|
participant "FcmService" as FCM
|
||||||
|
participant "LocationBroadcaster" as WS
|
||||||
|
participant "GuardianDashboard" as Dashboard
|
||||||
|
|
||||||
|
User -> UI : press SOS / voice command
|
||||||
|
UI -> UserController : POST /api/v1/user/sos
|
||||||
|
UserController -> SosService : triggerSos(userId, req)
|
||||||
|
SosService -> DB : save sos_events
|
||||||
|
SosService -> FCM : push high-priority SOS
|
||||||
|
SosService -> WS : /queue/sos/{guardianId}
|
||||||
|
WS -> Dashboard : realtime SOS alert
|
||||||
|
Guardian -> Dashboard : acknowledge
|
||||||
|
Dashboard -> SosService : PUT /guardian/sos/{id}/acknowledge
|
||||||
|
SosService -> DB : status ACKNOWLEDGED
|
||||||
|
@enduml
|
||||||
7
ooad-docs/diagrams/state-sos-event.puml
Normal file
7
ooad-docs/diagrams/state-sos-event.puml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@startuml
|
||||||
|
[*] --> TRIGGERED : User sends SOS
|
||||||
|
TRIGGERED --> ACKNOWLEDGED : Guardian acknowledges
|
||||||
|
ACKNOWLEDGED --> RESOLVED : incident handled
|
||||||
|
TRIGGERED --> RESOLVED : auto/manual close
|
||||||
|
RESOLVED --> [*]
|
||||||
|
@enduml
|
||||||
45
ooad-docs/diagrams/use-case-diagram.puml
Normal file
45
ooad-docs/diagrams/use-case-diagram.puml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
actor "User\n(Visually Impaired)" as User
|
||||||
|
actor "Guardian" as Guardian
|
||||||
|
actor "Firebase FCM" as FCM
|
||||||
|
actor "Agora RTC" as Agora
|
||||||
|
actor "OpenStreetMap/OSRM" as Map
|
||||||
|
|
||||||
|
rectangle "WalkGuide System" {
|
||||||
|
usecase "Register/Login" as UCAuth
|
||||||
|
usecase "Pair Guardian and User" as UCPair
|
||||||
|
usecase "Start WalkGuide" as UCWalk
|
||||||
|
usecase "Detect Obstacle" as UCDetect
|
||||||
|
usecase "Report Location" as UCLoc
|
||||||
|
usecase "Trigger SOS" as UCSos
|
||||||
|
usecase "Read Notifications" as UCNotif
|
||||||
|
usecase "Call Partner" as UCCall
|
||||||
|
usecase "Monitor Dashboard" as UCDash
|
||||||
|
usecase "Configure AI, TTS,\nVoice Commands, Geofence" as UCConfig
|
||||||
|
usecase "Acknowledge SOS" as UCAck
|
||||||
|
usecase "Navigate Route" as UCNav
|
||||||
|
}
|
||||||
|
|
||||||
|
User --> UCAuth
|
||||||
|
Guardian --> UCAuth
|
||||||
|
Guardian --> UCPair
|
||||||
|
User --> UCPair
|
||||||
|
User --> UCWalk
|
||||||
|
UCWalk --> UCDetect : <<include>>
|
||||||
|
UCWalk --> UCLoc : <<include>>
|
||||||
|
User --> UCSos
|
||||||
|
User --> UCNotif
|
||||||
|
User --> UCCall
|
||||||
|
Guardian --> UCCall
|
||||||
|
Guardian --> UCDash
|
||||||
|
Guardian --> UCConfig
|
||||||
|
Guardian --> UCAck
|
||||||
|
User --> UCNav
|
||||||
|
|
||||||
|
UCSos --> FCM
|
||||||
|
UCNotif --> FCM
|
||||||
|
UCCall --> Agora
|
||||||
|
UCCall --> FCM
|
||||||
|
UCNav --> Map
|
||||||
|
@enduml
|
||||||
@ -8,6 +8,7 @@ import java.time.LocalDateTime;
|
|||||||
public class PairingStatusResponse {
|
public class PairingStatusResponse {
|
||||||
private Long pairingId;
|
private Long pairingId;
|
||||||
private String status; // PENDING, ACTIVE, REJECTED, NONE
|
private String status; // PENDING, ACTIVE, REJECTED, NONE
|
||||||
|
private Long pairedWithId; // ID partner yang dipair
|
||||||
private String pairedWithName; // nama Guardian (untuk User) atau nama User (untuk Guardian)
|
private String pairedWithName; // nama Guardian (untuk User) atau nama User (untuk Guardian)
|
||||||
private String pairedWithEmail;
|
private String pairedWithEmail;
|
||||||
private String uniqueUserId; // ID user yang di-pair
|
private String uniqueUserId; // ID user yang di-pair
|
||||||
|
|||||||
@ -207,6 +207,7 @@ public class PairingService {
|
|||||||
return PairingStatusResponse.builder()
|
return PairingStatusResponse.builder()
|
||||||
.pairingId(p.getId())
|
.pairingId(p.getId())
|
||||||
.status(p.getStatus().name())
|
.status(p.getStatus().name())
|
||||||
|
.pairedWithId("GUARDIAN".equals(viewerRole) ? user.getId() : guardian.getId())
|
||||||
.pairedWithName(pairedWithName)
|
.pairedWithName(pairedWithName)
|
||||||
.pairedWithEmail(pairedWithEmail)
|
.pairedWithEmail(pairedWithEmail)
|
||||||
.uniqueUserId(user.getUniqueUserId())
|
.uniqueUserId(user.getUniqueUserId())
|
||||||
|
|||||||
287
walkguide-backend/demo/src/main/resources/openapi.yaml
Normal file
287
walkguide-backend/demo/src/main/resources/openapi.yaml
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: WalkGuide API
|
||||||
|
version: 1.0.0
|
||||||
|
description: Design contract for WalkGuide Flutter and Spring Boot integration.
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8080/api/v1
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
schemas:
|
||||||
|
ApiResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success: { type: boolean }
|
||||||
|
data: { nullable: true }
|
||||||
|
message: { type: string }
|
||||||
|
timestamp: { type: string, format: date-time }
|
||||||
|
LoginRequest:
|
||||||
|
type: object
|
||||||
|
required: [email, password]
|
||||||
|
properties:
|
||||||
|
email: { type: string, format: email }
|
||||||
|
password: { type: string }
|
||||||
|
RegisterRequest:
|
||||||
|
type: object
|
||||||
|
required: [email, password, displayName, role]
|
||||||
|
properties:
|
||||||
|
email: { type: string, format: email }
|
||||||
|
password: { type: string, minLength: 6 }
|
||||||
|
displayName: { type: string }
|
||||||
|
role: { type: string, enum: [USER, GUARDIAN] }
|
||||||
|
PairingInviteRequest:
|
||||||
|
type: object
|
||||||
|
required: [uniqueUserId]
|
||||||
|
properties:
|
||||||
|
uniqueUserId: { type: string, minLength: 12, maxLength: 12 }
|
||||||
|
PairingRespondRequest:
|
||||||
|
type: object
|
||||||
|
required: [pairingId, accept]
|
||||||
|
properties:
|
||||||
|
pairingId: { type: integer, format: int64 }
|
||||||
|
accept: { type: boolean }
|
||||||
|
LocationUpdateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lat: { type: number, format: double }
|
||||||
|
lng: { type: number, format: double }
|
||||||
|
accuracy: { type: number, format: double }
|
||||||
|
speed: { type: number, format: double }
|
||||||
|
heading: { type: number, format: double }
|
||||||
|
CallTokenRequest:
|
||||||
|
type: object
|
||||||
|
required: [receiverId]
|
||||||
|
properties:
|
||||||
|
receiverId: { type: integer, format: int64 }
|
||||||
|
paths:
|
||||||
|
/auth/ping:
|
||||||
|
get:
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200": { description: Server health, content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } }
|
||||||
|
/auth/register:
|
||||||
|
post:
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/RegisterRequest" }
|
||||||
|
responses:
|
||||||
|
"200": { description: Registered }
|
||||||
|
/auth/login:
|
||||||
|
post:
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/LoginRequest" }
|
||||||
|
responses:
|
||||||
|
"200": { description: Logged in }
|
||||||
|
/auth/refresh:
|
||||||
|
post:
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200": { description: Token refreshed }
|
||||||
|
/auth/logout:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: Logged out }
|
||||||
|
/auth/fcm-token:
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: FCM token updated }
|
||||||
|
/shared/pairing/invite:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/PairingInviteRequest" }
|
||||||
|
responses:
|
||||||
|
"200": { description: Invite sent }
|
||||||
|
/shared/pairing/respond:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/PairingRespondRequest" }
|
||||||
|
responses:
|
||||||
|
"200": { description: Invite responded }
|
||||||
|
/shared/pairing/unpair:
|
||||||
|
delete:
|
||||||
|
responses:
|
||||||
|
"200": { description: Pairing removed }
|
||||||
|
/shared/pairing/status:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Pairing status }
|
||||||
|
/shared/call/token:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/CallTokenRequest" }
|
||||||
|
responses:
|
||||||
|
"200": { description: Agora token generated }
|
||||||
|
/shared/call/notify:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: Incoming call notification sent }
|
||||||
|
/shared/call/end:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: Call end notification sent }
|
||||||
|
/user/profile:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Current user profile }
|
||||||
|
/user/settings:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: User settings }
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: User settings updated }
|
||||||
|
/user/voice-commands:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Voice commands }
|
||||||
|
/user/shortcuts:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Hardware shortcuts }
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: Hardware shortcut updated }
|
||||||
|
/user/ai-config:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: AI config }
|
||||||
|
/user/location:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/LocationUpdateRequest" }
|
||||||
|
responses:
|
||||||
|
"200": { description: Location stored }
|
||||||
|
/user/obstacle:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: Obstacle stored }
|
||||||
|
/user/sos:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: SOS triggered }
|
||||||
|
/user/activity-logs:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: User activity logs }
|
||||||
|
/user/notifications:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: User notifications }
|
||||||
|
/user/notifications/unread-count:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Unread notification count }
|
||||||
|
/user/notifications/mark-all-read:
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: All notifications marked read }
|
||||||
|
/user/notifications/{id}/read:
|
||||||
|
put:
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema: { type: integer, format: int64 }
|
||||||
|
responses:
|
||||||
|
"200": { description: One notification marked read }
|
||||||
|
/user/walkguide/start:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: WalkGuide start logged }
|
||||||
|
/user/walkguide/stop:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: WalkGuide stop logged }
|
||||||
|
/guardian/dashboard:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Guardian dashboard }
|
||||||
|
/guardian/user-location:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user latest location }
|
||||||
|
/guardian/location-history:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user location history }
|
||||||
|
/guardian/activity-logs:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user activity logs }
|
||||||
|
/guardian/obstacle-logs:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user obstacle logs }
|
||||||
|
/guardian/notifications/send:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200": { description: Notification sent to paired user }
|
||||||
|
/guardian/sos-events:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user SOS events }
|
||||||
|
/guardian/sos/{id}/acknowledge:
|
||||||
|
put:
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema: { type: integer, format: int64 }
|
||||||
|
responses:
|
||||||
|
"200": { description: SOS acknowledged }
|
||||||
|
/guardian/ai-config:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user AI config }
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user AI config updated }
|
||||||
|
/guardian/voice-commands:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user voice commands }
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user voice command updated }
|
||||||
|
/guardian/shortcuts:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user shortcuts }
|
||||||
|
/guardian/geofence:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Geofence config }
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: Geofence config updated }
|
||||||
|
/guardian/user-settings:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user settings }
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200": { description: Paired user settings updated }
|
||||||
@ -10,23 +10,68 @@ class CallService {
|
|||||||
|
|
||||||
CallService(this._apiClient);
|
CallService(this._apiClient);
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> requestToken(String channelName) async {
|
Future<Map<String, dynamic>?> requestToken({required int receiverId}) async {
|
||||||
final res = await _apiClient.dio.post('/shared/call/token', data: {'channelName': channelName});
|
final res = await _apiClient.dio.post(
|
||||||
|
'/shared/call/token',
|
||||||
|
data: {'receiverId': receiverId},
|
||||||
|
);
|
||||||
final data = res.data['data'];
|
final data = res.data['data'];
|
||||||
return data is Map<String, dynamic> ? data : null;
|
return data is Map<String, dynamic> ? data : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notifyIncomingCall({required int receiverId, required String channelName}) async {
|
Future<int?> getPairedReceiverId() async {
|
||||||
|
final res = await _apiClient.dio.get('/pairing/status');
|
||||||
|
final data = res.data['data'];
|
||||||
|
if (data is! Map<String, dynamic>) return null;
|
||||||
|
final id = data['pairedWithId'];
|
||||||
|
return id is num ? id.toInt() : int.tryParse(id?.toString() ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> notifyIncomingCall({
|
||||||
|
required int receiverId,
|
||||||
|
required String channelName,
|
||||||
|
String? agoraToken,
|
||||||
|
int receiverUid = 0,
|
||||||
|
}) async {
|
||||||
await _apiClient.dio.post('/shared/call/notify', data: {
|
await _apiClient.dio.post('/shared/call/notify', data: {
|
||||||
'receiverId': receiverId,
|
'receiverId': receiverId,
|
||||||
'channelName': channelName,
|
'channelName': channelName,
|
||||||
|
'agoraToken': agoraToken,
|
||||||
|
'receiverUid': receiverUid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> joinChannel({required String channelName, int uid = 0}) async {
|
Future<bool> callPairedUser({int uid = 0}) async {
|
||||||
|
final receiverId = await getPairedReceiverId();
|
||||||
|
if (receiverId == null) return false;
|
||||||
|
|
||||||
|
final tokenData = await requestToken(receiverId: receiverId);
|
||||||
|
final channelName = tokenData?['channelName']?.toString();
|
||||||
|
final token = tokenData?['token']?.toString();
|
||||||
|
if (channelName == null || channelName.isEmpty) return false;
|
||||||
|
|
||||||
|
final joined = await joinChannel(
|
||||||
|
channelName: channelName,
|
||||||
|
token: token,
|
||||||
|
uid: uid,
|
||||||
|
);
|
||||||
|
if (joined) {
|
||||||
|
await notifyIncomingCall(
|
||||||
|
receiverId: receiverId,
|
||||||
|
channelName: channelName,
|
||||||
|
agoraToken: token,
|
||||||
|
receiverUid: uid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> joinChannel({
|
||||||
|
required String channelName,
|
||||||
|
String? token,
|
||||||
|
int uid = 0,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final tokenData = await requestToken(channelName);
|
|
||||||
final token = tokenData?['token']?.toString();
|
|
||||||
_engine ??= createAgoraRtcEngine();
|
_engine ??= createAgoraRtcEngine();
|
||||||
await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId));
|
await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId));
|
||||||
await _engine!.enableAudio();
|
await _engine!.enableAudio();
|
||||||
|
|||||||
@ -148,7 +148,7 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
|
|||||||
onSelected: (_) {
|
onSelected: (_) {
|
||||||
setState(() => _applyFilter(f));
|
setState(() => _applyFilter(f));
|
||||||
},
|
},
|
||||||
selectedColor: AppColors.primary.withOpacity(0.15),
|
selectedColor: AppColors.primary.withValues(alpha: 0.15),
|
||||||
checkmarkColor: AppColors.primary,
|
checkmarkColor: AppColors.primary,
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: selected ? AppColors.primary : AppColors.muted,
|
color: selected ? AppColors.primary : AppColors.muted,
|
||||||
@ -232,7 +232,7 @@ class _LogCard extends StatelessWidget {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: meta.color.withOpacity(0.12),
|
color: meta.color.withValues(alpha: 0.12),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(meta.icon, color: meta.color, size: 18),
|
child: Icon(meta.icon, color: meta.color, size: 18),
|
||||||
|
|||||||
@ -91,7 +91,7 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.12),
|
color: Colors.white.withValues(alpha: 0.12),
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously, prefer_const_constructors
|
||||||
// lib/features/call/call_screen.dart
|
// lib/features/call/call_screen.dart
|
||||||
//
|
//
|
||||||
// CallScreen — user memanggil Guardian via Agora
|
// CallScreen — user memanggil Guardian via Agora
|
||||||
@ -34,8 +34,6 @@ class CallScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _CallScreenState extends State<CallScreen>
|
class _CallScreenState extends State<CallScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
static const _channelName = 'walkguide-call';
|
|
||||||
|
|
||||||
_CallPhase _phase = _CallPhase.calling;
|
_CallPhase _phase = _CallPhase.calling;
|
||||||
bool _muted = false;
|
bool _muted = false;
|
||||||
bool _speakerOn = true;
|
bool _speakerOn = true;
|
||||||
@ -61,8 +59,7 @@ class _CallScreenState extends State<CallScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startCall() async {
|
Future<void> _startCall() async {
|
||||||
final joined =
|
final joined = await sl<CallService>().callPairedUser();
|
||||||
await sl<CallService>().joinChannel(channelName: _channelName);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously, deprecated_member_use
|
// ignore_for_file: use_build_context_synchronously, deprecated_member_use, prefer_const_constructors
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -36,7 +35,6 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
String? _error;
|
String? _error;
|
||||||
String _guardianName = 'Guardian';
|
String _guardianName = 'Guardian';
|
||||||
int _refreshCount = 0;
|
|
||||||
|
|
||||||
// ── Live location (WebSocket) ────────────────────────────────────────────────
|
// ── Live location (WebSocket) ────────────────────────────────────────────────
|
||||||
LatLng? _liveLatLng;
|
LatLng? _liveLatLng;
|
||||||
@ -156,7 +154,6 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
_liveLatLng = newLatLng;
|
_liveLatLng = newLatLng;
|
||||||
}
|
}
|
||||||
_loading = false;
|
_loading = false;
|
||||||
_refreshCount++;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If SOS pending, start flash
|
// If SOS pending, start flash
|
||||||
@ -672,7 +669,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
Border.all(color: const Color(0xFFE2E8F0), width: 1),
|
Border.all(color: const Color(0xFFE2E8F0), width: 1),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.04),
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 2)),
|
offset: const Offset(0, 2)),
|
||||||
],
|
],
|
||||||
@ -708,7 +705,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: const Color(0xFF1A56DB)
|
color: const Color(0xFF1A56DB)
|
||||||
.withOpacity(0.25 * _pulseAnim.value),
|
.withValues(alpha: 0.25 * _pulseAnim.value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
@ -760,11 +757,11 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 5),
|
horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.92),
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
blurRadius: 8),
|
blurRadius: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -801,7 +798,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
horizontal: 10, vertical: 5),
|
horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color:
|
||||||
const Color(0xFF1A56DB).withOpacity(0.9),
|
const Color(0xFF1A56DB).withValues(alpha: 0.9),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text('Buka Peta',
|
child: Text('Buka Peta',
|
||||||
@ -823,11 +820,11 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 6),
|
horizontal: 10, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.92),
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.08),
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
blurRadius: 6),
|
blurRadius: 6),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -859,7 +856,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.04),
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 2)),
|
offset: const Offset(0, 2)),
|
||||||
],
|
],
|
||||||
@ -872,7 +869,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 20,
|
radius: 20,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
const Color(0xFF1A56DB).withOpacity(0.1),
|
const Color(0xFF1A56DB).withValues(alpha: 0.1),
|
||||||
child: Text(
|
child: Text(
|
||||||
d.userName.isNotEmpty
|
d.userName.isNotEmpty
|
||||||
? d.userName[0].toUpperCase()
|
? d.userName[0].toUpperCase()
|
||||||
@ -1051,7 +1048,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
|||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.04),
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 2)),
|
offset: const Offset(0, 2)),
|
||||||
],
|
],
|
||||||
@ -1357,7 +1354,7 @@ class _KpiCard extends StatelessWidget {
|
|||||||
width: 1),
|
width: 1),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.03),
|
color: Colors.black.withValues(alpha: 0.03),
|
||||||
blurRadius: 8),
|
blurRadius: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1421,7 +1418,7 @@ class _ActivityTile extends StatelessWidget {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cfg.color.withOpacity(0.1),
|
color: cfg.color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(cfg.icon, size: 16, color: cfg.color),
|
child: Icon(cfg.icon, size: 16, color: cfg.color),
|
||||||
@ -1485,7 +1482,7 @@ class _QuickActionCard extends StatelessWidget {
|
|||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: item.color.withOpacity(0.1),
|
color: item.color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(7),
|
||||||
),
|
),
|
||||||
child: Icon(item.icon,
|
child: Icon(item.icon,
|
||||||
@ -1527,7 +1524,7 @@ class _SosBadge extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFFDC2626).withOpacity(0.4),
|
color: const Color(0xFFDC2626).withValues(alpha: 0.4),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
spreadRadius: 1),
|
spreadRadius: 1),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -100,7 +100,7 @@ class _NavState extends ChangeNotifier {
|
|||||||
'heading': pos.heading,
|
'heading': pos.heading,
|
||||||
})
|
})
|
||||||
.timeout(const Duration(seconds: 5))
|
.timeout(const Duration(seconds: 5))
|
||||||
.catchError((_) => null);
|
.ignore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── search Nominatim ─────────────────────────────────────────────────────
|
// ── search Nominatim ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -4,10 +4,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../app/injection_container.dart';
|
import '../../app/injection_container.dart';
|
||||||
import '../../core/constants/app_constants.dart';
|
|
||||||
import '../../core/network/api_client.dart';
|
import '../../core/network/api_client.dart';
|
||||||
import '../../core/services/tts_service.dart';
|
import '../../core/services/tts_service.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
@ -254,13 +252,13 @@ class _NotifCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: unread
|
color: unread
|
||||||
? AppColors.primary.withOpacity(0.3)
|
? AppColors.primary.withValues(alpha: 0.3)
|
||||||
: const Color(0xFFE2E8F0),
|
: const Color(0xFFE2E8F0),
|
||||||
width: unread ? 1.5 : 1,
|
width: unread ? 1.5 : 1,
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.04),
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@ -279,8 +277,8 @@ class _NotifCard extends StatelessWidget {
|
|||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isVoice
|
color: isVoice
|
||||||
? AppColors.success.withOpacity(0.12)
|
? AppColors.success.withValues(alpha: 0.12)
|
||||||
: AppColors.primary.withOpacity(0.12),
|
: AppColors.primary.withValues(alpha: 0.12),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously, deprecated_member_use
|
// ignore_for_file: use_build_context_synchronously, deprecated_member_use, prefer_const_constructors
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../app/injection_container.dart';
|
import '../../app/injection_container.dart';
|
||||||
import '../../core/network/api_client.dart';
|
import '../../core/network/api_client.dart';
|
||||||
@ -382,10 +381,8 @@ class _Page extends StatelessWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final List<Widget>? actions;
|
|
||||||
|
|
||||||
const _Page(
|
const _Page({required this.title, required this.child, this.subtitle});
|
||||||
{required this.title, required this.child, this.subtitle, this.actions});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -412,7 +409,6 @@ class _Page extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...?actions,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@ -2168,10 +2168,10 @@ class _MapStatus extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.92),
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 18)
|
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 18)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -2251,7 +2251,7 @@ class _Pill extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.16),
|
color: color.withValues(alpha: 0.16),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
border: Border.all(color: color)),
|
border: Border.all(color: color)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously, prefer_const_constructors, deprecated_member_use
|
||||||
// lib/features/settings/user_settings_screen.dart
|
// lib/features/settings/user_settings_screen.dart
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -146,10 +146,10 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
|||||||
Future<void> _logout() async {
|
Future<void> _logout() async {
|
||||||
await sl<SecureStorage>().clearAll();
|
await sl<SecureStorage>().clearAll();
|
||||||
context.read<AppCubit>().clearSession();
|
context.read<AppCubit>().clearSession();
|
||||||
unawaited(_api
|
_api
|
||||||
.post('/auth/logout')
|
.post('/auth/logout')
|
||||||
.timeout(const Duration(seconds: 3))
|
.timeout(const Duration(seconds: 3))
|
||||||
.catchError((_) => null));
|
.ignore();
|
||||||
if (mounted) context.go('/login');
|
if (mounted) context.go('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
// lib/features/sos/sos_screen.dart
|
// lib/features/sos/sos_screen.dart
|
||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously, prefer_const_constructors, curly_braces_in_flow_control_structures
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../app/injection_container.dart';
|
import '../../app/injection_container.dart';
|
||||||
import '../../core/network/api_client.dart';
|
import '../../core/network/api_client.dart';
|
||||||
@ -326,7 +325,7 @@ class _SosButton extends StatelessWidget {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
active ? const Color(0xFFB91C1C) : const Color(0xFFDC2626),
|
active ? const Color(0xFFB91C1C) : const Color(0xFFDC2626),
|
||||||
elevation: active ? 12 : 4,
|
elevation: active ? 12 : 4,
|
||||||
shadowColor: const Color(0xFFDC2626).withOpacity(0.5),
|
shadowColor: const Color(0xFFDC2626).withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -363,7 +362,7 @@ class _SendingIndicator extends StatelessWidget {
|
|||||||
dimension: 200,
|
dimension: 200,
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFDC2626).withOpacity(0.15),
|
color: const Color(0xFFDC2626).withValues(alpha: 0.15),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: const Color(0xFFDC2626), width: 3),
|
border: Border.all(color: const Color(0xFFDC2626), width: 3),
|
||||||
),
|
),
|
||||||
@ -505,7 +504,7 @@ class _SosEventTile extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor.withOpacity(0.1),
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(statusIcon, color: statusColor, size: 20),
|
child: Icon(statusIcon, color: statusColor, size: 20),
|
||||||
@ -521,7 +520,7 @@ class _SosEventTile extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8, vertical: 2),
|
horizontal: 8, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor.withOpacity(0.1),
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(99),
|
borderRadius: BorderRadius.circular(99),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@ -466,7 +466,7 @@ class _Pill extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.16),
|
color: color.withValues(alpha: 0.16),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
border: Border.all(color: color)),
|
border: Border.all(color: color)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -506,12 +506,10 @@ class _EmptyPanel extends StatelessWidget {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
final Widget? action;
|
|
||||||
const _EmptyPanel(
|
const _EmptyPanel(
|
||||||
{required this.icon,
|
{required this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.message,
|
required this.message});
|
||||||
this.action});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -533,10 +531,6 @@ class _EmptyPanel extends StatelessWidget {
|
|||||||
const TextStyle(fontWeight: FontWeight.w800, fontSize: 18)),
|
const TextStyle(fontWeight: FontWeight.w800, fontSize: 18)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(message, textAlign: TextAlign.center),
|
Text(message, textAlign: TextAlign.center),
|
||||||
if (action != null) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
action!,
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -800,7 +800,7 @@ packages:
|
|||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
@ -947,10 +947,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.16.0"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1592,26 +1592,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.26.3"
|
version: "1.26.2"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.6"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.12"
|
version: "0.6.11"
|
||||||
tflite_flutter:
|
tflite_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -75,6 +75,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
|
intl: ^0.20.2
|
||||||
|
|
||||||
# STOMP client untuk WebSocket
|
# STOMP client untuk WebSocket
|
||||||
stomp_dart_client: ^2.1.0
|
stomp_dart_client: ^2.1.0
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// integration_test/app_flow_test.dart
|
// integration_test/app_flow_test.dart
|
||||||
//
|
// ignore_for_file: prefer_const_constructors, no_leading_underscores_for_local_identifiers
|
||||||
|
//
|
||||||
// Integration Tests (E2E) untuk WalkGuide App — 3 alur utama:
|
// Integration Tests (E2E) untuk WalkGuide App — 3 alur utama:
|
||||||
// Flow 1: Login → Dashboard → Logout
|
// Flow 1: Login → Dashboard → Logout
|
||||||
// Flow 2: Login → WalkGuide → Start → Stop → SOS
|
// Flow 2: Login → WalkGuide → Start → Stop → SOS
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// test/unit/obstacle_analyzer_test.dart
|
// test/unit/obstacle_analyzer_test.dart
|
||||||
//
|
// ignore_for_file: prefer_const_declarations
|
||||||
|
//
|
||||||
// Unit test untuk ObstacleAnalyzer — logika AI direction & distance.
|
// Unit test untuk ObstacleAnalyzer — logika AI direction & distance.
|
||||||
// Jalankan: flutter test test/unit/obstacle_analyzer_test.dart
|
// Jalankan: flutter test test/unit/obstacle_analyzer_test.dart
|
||||||
//
|
//
|
||||||
|
|||||||
@ -51,14 +51,14 @@ class _FakeStt {
|
|||||||
// VoiceCommandHandler (mirror dari project)
|
// VoiceCommandHandler (mirror dari project)
|
||||||
typedef CommandCallback = void Function(VoiceCommandKey key);
|
typedef CommandCallback = void Function(VoiceCommandKey key);
|
||||||
|
|
||||||
class VoiceCommandHandler {
|
class _VoiceCommandHandler {
|
||||||
final _FakeStt _stt;
|
final _FakeStt _stt;
|
||||||
final _FakeTts _tts;
|
final _FakeTts _tts;
|
||||||
|
|
||||||
List<VoiceCommand> _commands = [];
|
List<VoiceCommand> _commands = [];
|
||||||
CommandCallback? onCommand;
|
CommandCallback? onCommand;
|
||||||
|
|
||||||
VoiceCommandHandler(this._stt, this._tts);
|
_VoiceCommandHandler(this._stt, this._tts);
|
||||||
|
|
||||||
void loadCommands(List<VoiceCommand> commands) {
|
void loadCommands(List<VoiceCommand> commands) {
|
||||||
_commands = commands;
|
_commands = commands;
|
||||||
@ -137,7 +137,7 @@ class VoiceCommandHandler {
|
|||||||
// ---------- Tests ----------
|
// ---------- Tests ----------
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late VoiceCommandHandler handler;
|
late _VoiceCommandHandler handler;
|
||||||
late _FakeStt fakeStt;
|
late _FakeStt fakeStt;
|
||||||
late _FakeTts fakeTts;
|
late _FakeTts fakeTts;
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
fakeStt = _FakeStt();
|
fakeStt = _FakeStt();
|
||||||
fakeTts = _FakeTts();
|
fakeTts = _FakeTts();
|
||||||
handler = VoiceCommandHandler(fakeStt, fakeTts);
|
handler = _VoiceCommandHandler(fakeStt, fakeTts);
|
||||||
dispatchedCommands.clear();
|
dispatchedCommands.clear();
|
||||||
handler.loadDefaultCommands();
|
handler.loadDefaultCommands();
|
||||||
handler.onCommand = dispatchedCommands.add;
|
handler.onCommand = dispatchedCommands.add;
|
||||||
|
|||||||
@ -151,8 +151,6 @@ class _StubLoginScreenState extends State<_StubLoginScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Workaround untuk SizedBox(height(12)) yang error — gunakan helper
|
// Workaround untuk SizedBox(height(12)) yang error — gunakan helper
|
||||||
SizedBox _gap(double h) => SizedBox(height: h);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -397,4 +395,4 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// test/widget/manual_screen_test.dart
|
// test/widget/manual_screen_test.dart
|
||||||
//
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
//
|
||||||
// Widget tests untuk ManualScreen — halaman panduan perintah suara.
|
// Widget tests untuk ManualScreen — halaman panduan perintah suara.
|
||||||
// Jalankan: flutter test test/widget/manual_screen_test.dart
|
// Jalankan: flutter test test/widget/manual_screen_test.dart
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// test/widget/notification_screen_test.dart
|
// test/widget/notification_screen_test.dart
|
||||||
//
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
//
|
||||||
// Widget tests untuk NotificationScreen — menampilkan notifikasi dari Guardian.
|
// Widget tests untuk NotificationScreen — menampilkan notifikasi dari Guardian.
|
||||||
// Jalankan: flutter test test/widget/notification_screen_test.dart
|
// Jalankan: flutter test test/widget/notification_screen_test.dart
|
||||||
|
|
||||||
|
|||||||
@ -167,7 +167,7 @@ class _StubSosScreenState extends State<_StubSosScreen> {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.red.withOpacity(0.4),
|
color: Colors.red.withValues(alpha: 0.4),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
spreadRadius: 4,
|
spreadRadius: 4,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// test/widget/walk_guide_screen_test.dart
|
// test/widget/walk_guide_screen_test.dart
|
||||||
//
|
// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables
|
||||||
|
//
|
||||||
// Widget tests untuk WalkGuideScreen — layar utama navigasi tunanetra.
|
// Widget tests untuk WalkGuideScreen — layar utama navigasi tunanetra.
|
||||||
// Jalankan: flutter test test/widget/walk_guide_screen_test.dart
|
// Jalankan: flutter test test/widget/walk_guide_screen_test.dart
|
||||||
//
|
//
|
||||||
@ -264,7 +265,7 @@ class _DetectionOverlay extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: detection.isAlert ? Colors.red.withOpacity(0.85) : Colors.black87,
|
color: detection.isAlert ? Colors.red.withValues(alpha: 0.85) : Colors.black87,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -578,4 +579,4 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user