Fix local dev config and ALOTTTT OFF FLUTTER, im tired boss..

This commit is contained in:
Wowieee4
2026-05-26 21:09:13 +07:00
parent b8ad8df993
commit a629357e8c
72 changed files with 2566 additions and 530 deletions
@@ -0,0 +1,64 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/ai/obstacle_analyzer.dart';
import '../domain/repositories/walk_guide_repository.dart';
class WalkGuideState {
final bool active;
final DetectionResult? latestDetection;
final String status;
const WalkGuideState({
this.active = false,
this.latestDetection,
this.status = 'Ready',
});
WalkGuideState copyWith({
bool? active,
DetectionResult? latestDetection,
String? status,
}) {
return WalkGuideState(
active: active ?? this.active,
latestDetection: latestDetection ?? this.latestDetection,
status: status ?? this.status,
);
}
}
class WalkGuideCubit extends Cubit<WalkGuideState> {
final WalkGuideRepository _repository;
WalkGuideCubit(this._repository) : super(const WalkGuideState());
Future<void> start() async {
emit(state.copyWith(active: true, status: 'WalkGuide active'));
await _repository.startSession();
}
Future<void> stop() async {
emit(state.copyWith(active: false, status: 'WalkGuide stopped'));
await _repository.stopSession();
}
void updateStatus(String status) {
emit(state.copyWith(status: status));
}
void clearDetection({String status = 'Ready'}) {
emit(WalkGuideState(active: state.active, status: status));
}
Future<void> recordObstacle(DetectionResult detection) async {
emit(state.copyWith(latestDetection: detection, status: detection.spokenId));
await _repository.logObstacle({
'label': detection.label,
'confidence': detection.confidence,
'direction': detection.directionName,
'estimatedDist': detection.estimatedDistance,
'lat': null,
'lng': null,
});
}
}
@@ -0,0 +1,46 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/services/offline_queue_service.dart';
import '../../domain/repositories/walk_guide_repository.dart';
class WalkGuideRepositoryImpl implements WalkGuideRepository {
final ApiClient _apiClient;
final OfflineQueueService _offlineQueue;
const WalkGuideRepositoryImpl(this._apiClient, this._offlineQueue);
@override
Future<Either<Failure, void>> startSession() {
return _post('/user/walkguide/start', const {});
}
@override
Future<Either<Failure, void>> stopSession() {
return _post('/user/walkguide/stop', const {});
}
@override
Future<Either<Failure, void>> logObstacle(Map<String, dynamic> payload) {
return _post('/user/obstacle', payload);
}
Future<Either<Failure, void>> _post(
String path,
Map<String, dynamic> payload,
) async {
try {
await _apiClient.dio.post(path, data: payload);
return const Right(null);
} catch (_) {
await _offlineQueue.enqueue(OfflineRequest(
method: 'POST',
path: path,
body: payload,
createdAt: DateTime.now(),
));
return const Right(null);
}
}
}
@@ -0,0 +1,13 @@
class WalkSession {
final String sessionId;
final DateTime startedAt;
final DateTime? stoppedAt;
final int obstacleCount;
const WalkSession({
required this.sessionId,
required this.startedAt,
this.stoppedAt,
this.obstacleCount = 0,
});
}
@@ -0,0 +1,9 @@
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
abstract class WalkGuideRepository {
Future<Either<Failure, void>> startSession();
Future<Either<Failure, void>> stopSession();
Future<Either<Failure, void>> logObstacle(Map<String, dynamic> payload);
}
@@ -0,0 +1 @@
export '../../walk_guide_screen.dart';
@@ -5,14 +5,16 @@ import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../app/injection_container.dart';
import '../../core/ai/detection_export.dart';
import '../../core/network/api_client.dart';
import '../../core/services/haptic_service.dart';
import '../../core/ai/obstacle_alert_strategy.dart';
import '../../core/errors/friendly_error.dart';
import '../../core/services/location_reporter_service.dart';
import '../../core/services/tts_service.dart';
import 'application/walk_guide_cubit.dart';
// ---------------------------------------------------------------------------
// WalkGuideScreen
@@ -26,15 +28,19 @@ class WalkGuideScreen extends StatefulWidget {
}
class _WalkGuideScreenState extends State<WalkGuideScreen> {
bool _active = false;
String _status = 'Ready';
late final WalkGuideCubit _cubit;
CameraController? _camera;
DetectionResult? _lastDetection;
bool _processingFrame = false;
DateTime _lastInferenceAt = DateTime.fromMillisecondsSinceEpoch(0);
DateTime _lastAlertAt = DateTime.fromMillisecondsSinceEpoch(0);
DateTime _lastModelWarningAt = DateTime.fromMillisecondsSinceEpoch(0);
@override
void initState() {
super.initState();
_cubit = sl<WalkGuideCubit>();
}
@override
void dispose() {
final camera = _camera;
@@ -43,25 +49,23 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
}
_camera?.dispose();
sl<LocationReporterService>().stop();
_cubit.close();
super.dispose();
}
Future<void> _toggle() async {
final next = !_active;
final next = !_cubit.state.active;
if (next) {
await _startCamera();
await sl<LocationReporterService>().start(walkGuideActive: true);
await _cubit.start();
_cubit.updateStatus(_activeStatusText());
} else {
await _stopCamera();
await sl<LocationReporterService>().stop();
await _cubit.stop();
_cubit.clearDetection(status: 'Stopped');
}
setState(() {
_active = next;
_status = next ? _activeStatusText() : 'Stopped';
});
await sl<ApiClient>()
.dio
.post(next ? '/user/walkguide/start' : '/user/walkguide/stop');
sl<TtsService>().speak(next ? 'WalkGuide dimulai' : 'WalkGuide dihentikan');
}
@@ -80,7 +84,8 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
Future<void> _startCamera() async {
if (_camera != null) return;
try {
await runFriendlyAction(
() async {
final cameras = await availableCameras();
if (cameras.isEmpty) return;
final backCamera = cameras.firstWhere(
@@ -98,33 +103,40 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
await controller.dispose();
return;
}
try {
await controller.startImageStream(_onCameraImage);
} catch (_) {
setState(() => _status = kIsWeb
? 'Camera preview aktif, tapi image stream YOLO tidak tersedia di Chrome/web.'
: 'Camera preview aktif, tapi image stream belum tersedia.');
}
await runFriendlyAction(
() => controller.startImageStream(_onCameraImage),
onError: (_) {
_cubit.updateStatus(kIsWeb
? 'Camera preview aktif, tapi image stream YOLO tidak tersedia di Chrome/web.'
: 'Camera preview aktif, tapi image stream belum tersedia.');
},
fallback: 'Camera preview aktif, tapi image stream belum tersedia.',
);
setState(() => _camera = controller);
} catch (_) {
setState(() => _status = 'Camera unavailable.');
}
},
onError: (_) => _cubit.updateStatus('Camera unavailable.'),
fallback: 'Camera unavailable.',
);
}
Future<void> _stopCamera() async {
final camera = _camera;
_camera = null;
if (camera == null) return;
try {
if (camera.value.isStreamingImages) {
await camera.stopImageStream();
}
} catch (_) {}
await runFriendlyAction(
() async {
if (camera.value.isStreamingImages) {
await camera.stopImageStream();
}
},
onError: (_) {},
fallback: 'Camera stream already stopped.',
);
await camera.dispose();
}
void _onCameraImage(CameraImage image) {
if (!_active || _processingFrame) return;
if (!_cubit.state.active || _processingFrame) return;
final now = DateTime.now();
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 900)) {
return;
@@ -141,7 +153,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
final now = DateTime.now();
if (now.difference(_lastModelWarningAt) > const Duration(seconds: 3)) {
_lastModelWarningAt = now;
setState(() => _status = detector.isReady
_cubit.updateStatus(detector.isReady
? 'Scanning... ${detector.diagnosticsSummary}'
: 'YOLO model belum siap. Pastikan file model tersedia di assets/models.');
}
@@ -154,8 +166,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
DetectionResult detection, {
bool forceAlert = false,
}) async {
_lastDetection = detection;
setState(() => _status =
_cubit.updateStatus(
'Obstacle: ${ObstacleAnalyzer.spokenLabel(detection.label)} ${detection.directionName} ${detection.estimatedDistance}');
final now = DateTime.now();
@@ -165,94 +176,94 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
}
_lastAlertAt = now;
try {
await sl<ApiClient>().dio.post('/user/obstacle', data: {
'label': detection.label,
'confidence': detection.confidence,
'direction': detection.directionName,
'estimatedDist': detection.estimatedDistance,
'lat': null,
'lng': null,
});
} catch (_) {}
await sl<HapticService>().obstacleClose();
await sl<TtsService>().speakImmediate(detection.spokenId);
await runFriendlyAction(
() => _cubit.recordObstacle(detection),
onError: (_) {},
fallback: 'Obstacle tersimpan offline.',
);
await sl<ObstacleAlertStrategy>().alert(detection);
}
@override
Widget build(BuildContext context) {
return _Page(
title: 'WalkGuide',
subtitle: 'On-device AI detection surface',
actions: [
IconButton(
onPressed: () => context.go('/user/benchmark'),
icon: const Icon(Icons.speed)),
IconButton(
onPressed: () => context.go('/user/pairing'),
icon: const Icon(Icons.link)),
],
child: Column(
children: [
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(16)),
child: Stack(
children: [
if (_camera != null && _camera!.value.isInitialized)
Positioned.fill(child: CameraPreview(_camera!))
else
const Center(
child: Icon(Icons.videocam_outlined,
color: Colors.white30, size: 96)),
if (_lastDetection?.box != null)
Positioned.fill(
child: CustomPaint(
painter: _DetectionOverlayPainter(_lastDetection!),
return BlocBuilder<WalkGuideCubit, WalkGuideState>(
bloc: _cubit,
builder: (context, state) => _Page(
title: 'WalkGuide',
subtitle: 'On-device AI detection surface',
actions: [
IconButton(
onPressed: () => context.go('/user/benchmark'),
icon: const Icon(Icons.speed)),
IconButton(
onPressed: () => context.go('/user/pairing'),
icon: const Icon(Icons.link)),
],
child: Column(
children: [
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(16)),
child: Stack(
children: [
if (_camera != null && _camera!.value.isInitialized)
Positioned.fill(child: CameraPreview(_camera!))
else
const Center(
child: Icon(Icons.videocam_outlined,
color: Colors.white30, size: 96)),
if (state.latestDetection?.box != null)
Positioned.fill(
child: CustomPaint(
painter:
_DetectionOverlayPainter(state.latestDetection!),
),
),
),
Positioned(
top: 16,
left: 16,
child: _Pill(
text: _active ? 'AI ACTIVE' : 'STANDBY',
color: _active ? Colors.green : Colors.orange)),
if (_lastDetection != null)
Positioned(
top: 64,
left: 16,
child: _Pill(
text:
'${ObstacleAnalyzer.spokenLabel(_lastDetection!.label)} ${_lastDetection!.directionName}',
color: Colors.redAccent),
),
Positioned(
left: 16,
right: 16,
bottom: 16,
child: Text(_status,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700))),
],
top: 16,
left: 16,
child: _Pill(
text: state.active ? 'AI ACTIVE' : 'STANDBY',
color:
state.active ? Colors.green : Colors.orange)),
if (state.latestDetection != null)
Positioned(
top: 64,
left: 16,
child: _Pill(
text:
'${ObstacleAnalyzer.spokenLabel(state.latestDetection!.label)} ${state.latestDetection!.directionName}',
color: Colors.redAccent),
),
Positioned(
left: 16,
right: 16,
bottom: 16,
child: Text(state.status,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700))),
],
),
),
),
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _toggle,
icon: Icon(_active ? Icons.stop : Icons.play_arrow),
label: Text(_active ? 'Stop' : 'Start'))),
],
),
],
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: _toggle,
icon:
Icon(state.active ? Icons.stop : Icons.play_arrow),
label: Text(state.active ? 'Stop' : 'Start'))),
],
),
],
),
),
);
}