fixing a ton of sloppy mess...

This commit is contained in:
Wowieee4 2026-05-17 18:40:03 +07:00
parent f970f1bac8
commit d2608e0188
37 changed files with 3099 additions and 1795 deletions

File diff suppressed because it is too large Load Diff

View 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
```

View 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.

View 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.

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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())

View 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 }

View File

@ -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();

View File

@ -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),

View File

@ -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(

View File

@ -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;

View File

@ -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),
], ],

View File

@ -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

View File

@ -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(

View File

@ -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),

View File

@ -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(

View File

@ -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');
} }

View File

@ -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(

View File

@ -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!,
],
], ],
), ),
); );

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
// //

View File

@ -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;

View File

@ -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() {
}); });
}); });
}); });
} }

View File

@ -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

View File

@ -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

View File

@ -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,
), ),

View File

@ -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() {
}); });
}); });
}); });
} }