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 {
|
||||
private Long pairingId;
|
||||
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 pairedWithEmail;
|
||||
private String uniqueUserId; // ID user yang di-pair
|
||||
|
||||
@ -207,6 +207,7 @@ public class PairingService {
|
||||
return PairingStatusResponse.builder()
|
||||
.pairingId(p.getId())
|
||||
.status(p.getStatus().name())
|
||||
.pairedWithId("GUARDIAN".equals(viewerRole) ? user.getId() : guardian.getId())
|
||||
.pairedWithName(pairedWithName)
|
||||
.pairedWithEmail(pairedWithEmail)
|
||||
.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);
|
||||
|
||||
Future<Map<String, dynamic>?> requestToken(String channelName) async {
|
||||
final res = await _apiClient.dio.post('/shared/call/token', data: {'channelName': channelName});
|
||||
Future<Map<String, dynamic>?> requestToken({required int receiverId}) async {
|
||||
final res = await _apiClient.dio.post(
|
||||
'/shared/call/token',
|
||||
data: {'receiverId': receiverId},
|
||||
);
|
||||
final data = res.data['data'];
|
||||
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: {
|
||||
'receiverId': receiverId,
|
||||
'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 {
|
||||
final tokenData = await requestToken(channelName);
|
||||
final token = tokenData?['token']?.toString();
|
||||
_engine ??= createAgoraRtcEngine();
|
||||
await _engine!.initialize(const RtcEngineContext(appId: AppConstants.agoraAppId));
|
||||
await _engine!.enableAudio();
|
||||
|
||||
@ -148,7 +148,7 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
|
||||
onSelected: (_) {
|
||||
setState(() => _applyFilter(f));
|
||||
},
|
||||
selectedColor: AppColors.primary.withOpacity(0.15),
|
||||
selectedColor: AppColors.primary.withValues(alpha: 0.15),
|
||||
checkmarkColor: AppColors.primary,
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? AppColors.primary : AppColors.muted,
|
||||
@ -232,7 +232,7 @@ class _LogCard extends StatelessWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: meta.color.withOpacity(0.12),
|
||||
color: meta.color.withValues(alpha: 0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(meta.icon, color: meta.color, size: 18),
|
||||
|
||||
@ -91,7 +91,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.12),
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
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
|
||||
//
|
||||
// CallScreen — user memanggil Guardian via Agora
|
||||
@ -34,8 +34,6 @@ class CallScreen extends StatefulWidget {
|
||||
|
||||
class _CallScreenState extends State<CallScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const _channelName = 'walkguide-call';
|
||||
|
||||
_CallPhase _phase = _CallPhase.calling;
|
||||
bool _muted = false;
|
||||
bool _speakerOn = true;
|
||||
@ -61,8 +59,7 @@ class _CallScreenState extends State<CallScreen>
|
||||
}
|
||||
|
||||
Future<void> _startCall() async {
|
||||
final joined =
|
||||
await sl<CallService>().joinChannel(channelName: _channelName);
|
||||
final joined = await sl<CallService>().callPairedUser();
|
||||
|
||||
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:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -36,7 +35,6 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
String _guardianName = 'Guardian';
|
||||
int _refreshCount = 0;
|
||||
|
||||
// ── Live location (WebSocket) ────────────────────────────────────────────────
|
||||
LatLng? _liveLatLng;
|
||||
@ -156,7 +154,6 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
_liveLatLng = newLatLng;
|
||||
}
|
||||
_loading = false;
|
||||
_refreshCount++;
|
||||
});
|
||||
|
||||
// If SOS pending, start flash
|
||||
@ -672,7 +669,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
Border.all(color: const Color(0xFFE2E8F0), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 2)),
|
||||
],
|
||||
@ -708,7 +705,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFF1A56DB)
|
||||
.withOpacity(0.25 * _pulseAnim.value),
|
||||
.withValues(alpha: 0.25 * _pulseAnim.value),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@ -760,11 +757,11 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.92),
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 8),
|
||||
],
|
||||
),
|
||||
@ -801,7 +798,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
const Color(0xFF1A56DB).withOpacity(0.9),
|
||||
const Color(0xFF1A56DB).withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text('Buka Peta',
|
||||
@ -823,11 +820,11 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.92),
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 6),
|
||||
],
|
||||
),
|
||||
@ -859,7 +856,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 2)),
|
||||
],
|
||||
@ -872,7 +869,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor:
|
||||
const Color(0xFF1A56DB).withOpacity(0.1),
|
||||
const Color(0xFF1A56DB).withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
d.userName.isNotEmpty
|
||||
? d.userName[0].toUpperCase()
|
||||
@ -1051,7 +1048,7 @@ class _GuardianDashboardScreenState extends State<GuardianDashboardScreen>
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 2)),
|
||||
],
|
||||
@ -1357,7 +1354,7 @@ class _KpiCard extends StatelessWidget {
|
||||
width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 8),
|
||||
],
|
||||
),
|
||||
@ -1421,7 +1418,7 @@ class _ActivityTile extends StatelessWidget {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: cfg.color.withOpacity(0.1),
|
||||
color: cfg.color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(cfg.icon, size: 16, color: cfg.color),
|
||||
@ -1485,7 +1482,7 @@ class _QuickActionCard extends StatelessWidget {
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withOpacity(0.1),
|
||||
color: item.color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Icon(item.icon,
|
||||
@ -1527,7 +1524,7 @@ class _SosBadge extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFDC2626).withOpacity(0.4),
|
||||
color: const Color(0xFFDC2626).withValues(alpha: 0.4),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 1),
|
||||
],
|
||||
|
||||
@ -100,7 +100,7 @@ class _NavState extends ChangeNotifier {
|
||||
'heading': pos.heading,
|
||||
})
|
||||
.timeout(const Duration(seconds: 5))
|
||||
.catchError((_) => null);
|
||||
.ignore();
|
||||
}
|
||||
|
||||
// ── search Nominatim ─────────────────────────────────────────────────────
|
||||
|
||||
@ -4,10 +4,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../app/injection_container.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/services/tts_service.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
@ -254,13 +252,13 @@ class _NotifCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: unread
|
||||
? AppColors.primary.withOpacity(0.3)
|
||||
? AppColors.primary.withValues(alpha: 0.3)
|
||||
: const Color(0xFFE2E8F0),
|
||||
width: unread ? 1.5 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@ -279,8 +277,8 @@ class _NotifCard extends StatelessWidget {
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isVoice
|
||||
? AppColors.success.withOpacity(0.12)
|
||||
: AppColors.primary.withOpacity(0.12),
|
||||
? AppColors.success.withValues(alpha: 0.12)
|
||||
: AppColors.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
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 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../app/injection_container.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
@ -382,10 +381,8 @@ class _Page extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget child;
|
||||
final List<Widget>? actions;
|
||||
|
||||
const _Page(
|
||||
{required this.title, required this.child, this.subtitle, this.actions});
|
||||
const _Page({required this.title, required this.child, this.subtitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -412,7 +409,6 @@ class _Page extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
...?actions,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@ -2168,10 +2168,10 @@ class _MapStatus extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.92),
|
||||
color: Colors.white.withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 18)
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 18)
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
@ -2251,7 +2251,7 @@ class _Pill extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.16),
|
||||
color: color.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: color)),
|
||||
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
|
||||
|
||||
import 'dart:async';
|
||||
@ -146,10 +146,10 @@ class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
||||
Future<void> _logout() async {
|
||||
await sl<SecureStorage>().clearAll();
|
||||
context.read<AppCubit>().clearSession();
|
||||
unawaited(_api
|
||||
_api
|
||||
.post('/auth/logout')
|
||||
.timeout(const Duration(seconds: 3))
|
||||
.catchError((_) => null));
|
||||
.ignore();
|
||||
if (mounted) context.go('/login');
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
// 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 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../app/injection_container.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
@ -326,7 +325,7 @@ class _SosButton extends StatelessWidget {
|
||||
backgroundColor:
|
||||
active ? const Color(0xFFB91C1C) : const Color(0xFFDC2626),
|
||||
elevation: active ? 12 : 4,
|
||||
shadowColor: const Color(0xFFDC2626).withOpacity(0.5),
|
||||
shadowColor: const Color(0xFFDC2626).withValues(alpha: 0.5),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: Column(
|
||||
@ -363,7 +362,7 @@ class _SendingIndicator extends StatelessWidget {
|
||||
dimension: 200,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDC2626).withOpacity(0.15),
|
||||
color: const Color(0xFFDC2626).withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFFDC2626), width: 3),
|
||||
),
|
||||
@ -505,7 +504,7 @@ class _SosEventTile extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(statusIcon, color: statusColor, size: 20),
|
||||
@ -521,7 +520,7 @@ class _SosEventTile extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
@ -466,7 +466,7 @@ class _Pill extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.16),
|
||||
color: color.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: color)),
|
||||
child: Padding(
|
||||
@ -506,12 +506,10 @@ class _EmptyPanel extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String message;
|
||||
final Widget? action;
|
||||
const _EmptyPanel(
|
||||
{required this.icon,
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.action});
|
||||
required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -533,10 +531,6 @@ class _EmptyPanel extends StatelessWidget {
|
||||
const TextStyle(fontWeight: FontWeight.w800, fontSize: 18)),
|
||||
const SizedBox(height: 6),
|
||||
Text(message, textAlign: TextAlign.center),
|
||||
if (action != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
action!,
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -800,7 +800,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
@ -947,10 +947,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.16.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1592,26 +1592,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.26.3"
|
||||
version: "1.26.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.6"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.12"
|
||||
version: "0.6.11"
|
||||
tflite_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -75,6 +75,7 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
cached_network_image: ^3.3.1
|
||||
shimmer: ^3.0.0
|
||||
intl: ^0.20.2
|
||||
|
||||
# STOMP client untuk WebSocket
|
||||
stomp_dart_client: ^2.1.0
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// 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:
|
||||
// Flow 1: Login → Dashboard → Logout
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// test/unit/obstacle_analyzer_test.dart
|
||||
// ignore_for_file: prefer_const_declarations
|
||||
//
|
||||
// Unit test untuk ObstacleAnalyzer — logika AI direction & distance.
|
||||
// Jalankan: flutter test test/unit/obstacle_analyzer_test.dart
|
||||
|
||||
@ -51,14 +51,14 @@ class _FakeStt {
|
||||
// VoiceCommandHandler (mirror dari project)
|
||||
typedef CommandCallback = void Function(VoiceCommandKey key);
|
||||
|
||||
class VoiceCommandHandler {
|
||||
class _VoiceCommandHandler {
|
||||
final _FakeStt _stt;
|
||||
final _FakeTts _tts;
|
||||
|
||||
List<VoiceCommand> _commands = [];
|
||||
CommandCallback? onCommand;
|
||||
|
||||
VoiceCommandHandler(this._stt, this._tts);
|
||||
_VoiceCommandHandler(this._stt, this._tts);
|
||||
|
||||
void loadCommands(List<VoiceCommand> commands) {
|
||||
_commands = commands;
|
||||
@ -137,7 +137,7 @@ class VoiceCommandHandler {
|
||||
// ---------- Tests ----------
|
||||
|
||||
void main() {
|
||||
late VoiceCommandHandler handler;
|
||||
late _VoiceCommandHandler handler;
|
||||
late _FakeStt fakeStt;
|
||||
late _FakeTts fakeTts;
|
||||
|
||||
@ -146,7 +146,7 @@ void main() {
|
||||
setUp(() {
|
||||
fakeStt = _FakeStt();
|
||||
fakeTts = _FakeTts();
|
||||
handler = VoiceCommandHandler(fakeStt, fakeTts);
|
||||
handler = _VoiceCommandHandler(fakeStt, fakeTts);
|
||||
dispatchedCommands.clear();
|
||||
handler.loadDefaultCommands();
|
||||
handler.onCommand = dispatchedCommands.add;
|
||||
|
||||
@ -151,8 +151,6 @@ class _StubLoginScreenState extends State<_StubLoginScreen> {
|
||||
}
|
||||
|
||||
// Workaround untuk SizedBox(height(12)) yang error — gunakan helper
|
||||
SizedBox _gap(double h) => SizedBox(height: h);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// test/widget/manual_screen_test.dart
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
//
|
||||
// Widget tests untuk ManualScreen — halaman panduan perintah suara.
|
||||
// Jalankan: flutter test test/widget/manual_screen_test.dart
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// test/widget/notification_screen_test.dart
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
//
|
||||
// Widget tests untuk NotificationScreen — menampilkan notifikasi dari Guardian.
|
||||
// Jalankan: flutter test test/widget/notification_screen_test.dart
|
||||
|
||||
@ -167,7 +167,7 @@ class _StubSosScreenState extends State<_StubSosScreen> {
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.red.withOpacity(0.4),
|
||||
color: Colors.red.withValues(alpha: 0.4),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 4,
|
||||
),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// 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.
|
||||
// Jalankan: flutter test test/widget/walk_guide_screen_test.dart
|
||||
@ -264,7 +265,7 @@ class _DetectionOverlay extends StatelessWidget {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
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),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user