feat: integrate YOLO runtime, update screens, and sync docs
This commit is contained in:
@@ -48,8 +48,8 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
|
||||
final res = await _api
|
||||
.get('/user/activity-logs')
|
||||
.timeout(const Duration(seconds: 10));
|
||||
final list = (res.data['data'] as List?) ?? [];
|
||||
final items = list.map((e) => _LogItem.fromJson(e)).toList();
|
||||
final list = _extractList(res.data);
|
||||
final items = list.map(_LogItem.fromJson).toList();
|
||||
setState(() {
|
||||
_items = items;
|
||||
_applyFilter(_selectedFilter);
|
||||
@@ -66,6 +66,16 @@ class _ActivityLogScreenState extends State<ActivityLogScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _extractList(dynamic responseBody) {
|
||||
final data = responseBody is Map ? responseBody['data'] : null;
|
||||
final rawList = data is Map ? data['content'] : data;
|
||||
if (rawList is! List) return const [];
|
||||
return rawList
|
||||
.whereType<Map>()
|
||||
.map((item) => Map<String, dynamic>.from(item))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _applyFilter(String filter) {
|
||||
_selectedFilter = filter;
|
||||
if (filter == 'ALL') {
|
||||
|
||||
@@ -40,7 +40,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final res = await _api
|
||||
.get('/user/notifications')
|
||||
.timeout(const Duration(seconds: 10));
|
||||
final list = (res.data['data'] as List?) ?? [];
|
||||
final list = _extractList(res.data);
|
||||
setState(() {
|
||||
_items = list.map((e) => _NotifItem.fromJson(e)).toList();
|
||||
});
|
||||
@@ -56,10 +56,20 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _extractList(dynamic responseBody) {
|
||||
final data = responseBody is Map ? responseBody['data'] : null;
|
||||
final rawList = data is Map ? data['content'] : data;
|
||||
if (rawList is! List) return const [];
|
||||
return rawList
|
||||
.whereType<Map>()
|
||||
.map((item) => Map<String, dynamic>.from(item))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> _markRead(int id) async {
|
||||
try {
|
||||
await _api
|
||||
.patch('/user/notifications/$id/read')
|
||||
.put('/user/notifications/$id/read')
|
||||
.timeout(const Duration(seconds: 6));
|
||||
setState(() {
|
||||
final idx = _items.indexWhere((n) => n.id == id);
|
||||
@@ -72,7 +82,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
setState(() => _markingAll = true);
|
||||
try {
|
||||
await _api
|
||||
.patch('/user/notifications/read-all')
|
||||
.put('/user/notifications/mark-all-read')
|
||||
.timeout(const Duration(seconds: 8));
|
||||
setState(() {
|
||||
_items = _items.map((n) => n.copyWith(isRead: true)).toList();
|
||||
|
||||
@@ -356,9 +356,16 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
|
||||
String _status = 'Ready';
|
||||
CameraController? _camera;
|
||||
DetectionResult? _lastDetection;
|
||||
bool _processingFrame = false;
|
||||
DateTime _lastInferenceAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastAlertAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final camera = _camera;
|
||||
if (camera != null && camera.value.isStreamingImages) {
|
||||
unawaited(camera.stopImageStream().catchError((Object _) {}));
|
||||
}
|
||||
_camera?.dispose();
|
||||
sl<LocationReporterService>().stop();
|
||||
super.dispose();
|
||||
@@ -370,8 +377,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
|
||||
await _startCamera();
|
||||
await sl<LocationReporterService>().start(walkGuideActive: true);
|
||||
} else {
|
||||
await _camera?.dispose();
|
||||
_camera = null;
|
||||
await _stopCamera();
|
||||
await sl<LocationReporterService>().stop();
|
||||
}
|
||||
setState(() {
|
||||
@@ -388,35 +394,89 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
|
||||
final cameras = await availableCameras();
|
||||
if (cameras.isEmpty) return;
|
||||
final controller = CameraController(
|
||||
cameras.first, ResolutionPreset.medium,
|
||||
enableAudio: false);
|
||||
cameras.first,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.yuv420,
|
||||
);
|
||||
await controller.initialize();
|
||||
if (!mounted) {
|
||||
await controller.dispose();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await controller.startImageStream(_onCameraImage);
|
||||
} catch (_) {
|
||||
// Preview still works; manual Demo Detect remains available.
|
||||
}
|
||||
setState(() => _camera = controller);
|
||||
} catch (_) {
|
||||
setState(() => _status = 'Camera unavailable. Demo mode active.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopCamera() async {
|
||||
final camera = _camera;
|
||||
_camera = null;
|
||||
if (camera == null) return;
|
||||
try {
|
||||
if (camera.value.isStreamingImages) {
|
||||
await camera.stopImageStream();
|
||||
}
|
||||
} catch (_) {}
|
||||
await camera.dispose();
|
||||
}
|
||||
|
||||
void _onCameraImage(CameraImage image) {
|
||||
if (!_active || _processingFrame) return;
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 900)) {
|
||||
return;
|
||||
}
|
||||
_lastInferenceAt = now;
|
||||
_processingFrame = true;
|
||||
unawaited(_runYolo(image).whenComplete(() => _processingFrame = false));
|
||||
}
|
||||
|
||||
Future<void> _runYolo(CameraImage image) async {
|
||||
final detection = await sl<YoloDetector>().detect(image);
|
||||
if (detection == null || !mounted) return;
|
||||
await _handleDetection(detection);
|
||||
}
|
||||
|
||||
Future<void> _simulateObstacle() async {
|
||||
final detection = await sl<YoloDetector>().detectFallback();
|
||||
if (detection == null) return;
|
||||
await _handleDetection(detection, forceAlert: true);
|
||||
}
|
||||
|
||||
Future<void> _handleDetection(
|
||||
DetectionResult detection, {
|
||||
bool forceAlert = false,
|
||||
}) async {
|
||||
_lastDetection = detection;
|
||||
await _api.post('/user/obstacle', data: {
|
||||
'label': detection.label,
|
||||
'confidence': detection.confidence,
|
||||
'direction': detection.directionName,
|
||||
'estimatedDist': detection.estimatedDistance,
|
||||
'lat': null,
|
||||
'lng': null,
|
||||
});
|
||||
await sl<HapticService>().obstacleClose();
|
||||
await sl<TtsService>().speakImmediate(detection.spokenId);
|
||||
setState(() => _status =
|
||||
'Obstacle: ${detection.label} ${detection.directionName} ${detection.estimatedDistance}');
|
||||
|
||||
final now = DateTime.now();
|
||||
if (!forceAlert &&
|
||||
now.difference(_lastAlertAt) < const Duration(seconds: 4)) {
|
||||
return;
|
||||
}
|
||||
_lastAlertAt = now;
|
||||
|
||||
try {
|
||||
await _api.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);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1148,12 +1208,8 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
|
||||
String label = 'person';
|
||||
String direction = 'CENTER';
|
||||
String distance = 'Demo';
|
||||
var modelLoaded = false;
|
||||
try {
|
||||
await rootBundle.load(_selectedModel).timeout(const Duration(seconds: 3));
|
||||
modelLoaded = true;
|
||||
} catch (_) {}
|
||||
final detection = await sl<YoloDetector>().detectFallback();
|
||||
final modelLoaded = sl<YoloDetector>().isReady;
|
||||
final detection = await sl<YoloDetector>().detectSynthetic();
|
||||
if (detection != null) {
|
||||
label = detection.label;
|
||||
direction = detection.directionName;
|
||||
|
||||
@@ -33,9 +33,16 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
|
||||
String _status = 'Ready';
|
||||
CameraController? _camera;
|
||||
DetectionResult? _lastDetection;
|
||||
bool _processingFrame = false;
|
||||
DateTime _lastInferenceAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastAlertAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final camera = _camera;
|
||||
if (camera != null && camera.value.isStreamingImages) {
|
||||
unawaited(camera.stopImageStream().catchError((Object _) {}));
|
||||
}
|
||||
_camera?.dispose();
|
||||
sl<LocationReporterService>().stop();
|
||||
super.dispose();
|
||||
@@ -47,8 +54,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
|
||||
await _startCamera();
|
||||
await sl<LocationReporterService>().start(walkGuideActive: true);
|
||||
} else {
|
||||
await _camera?.dispose();
|
||||
_camera = null;
|
||||
await _stopCamera();
|
||||
await sl<LocationReporterService>().stop();
|
||||
}
|
||||
setState(() {
|
||||
@@ -67,35 +73,89 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
|
||||
final cameras = await availableCameras();
|
||||
if (cameras.isEmpty) return;
|
||||
final controller = CameraController(
|
||||
cameras.first, ResolutionPreset.medium,
|
||||
enableAudio: false);
|
||||
cameras.first,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.yuv420,
|
||||
);
|
||||
await controller.initialize();
|
||||
if (!mounted) {
|
||||
await controller.dispose();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await controller.startImageStream(_onCameraImage);
|
||||
} catch (_) {
|
||||
// Preview still works; manual Demo Detect remains available.
|
||||
}
|
||||
setState(() => _camera = controller);
|
||||
} catch (_) {
|
||||
setState(() => _status = 'Camera unavailable. Demo mode active.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopCamera() async {
|
||||
final camera = _camera;
|
||||
_camera = null;
|
||||
if (camera == null) return;
|
||||
try {
|
||||
if (camera.value.isStreamingImages) {
|
||||
await camera.stopImageStream();
|
||||
}
|
||||
} catch (_) {}
|
||||
await camera.dispose();
|
||||
}
|
||||
|
||||
void _onCameraImage(CameraImage image) {
|
||||
if (!_active || _processingFrame) return;
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 900)) {
|
||||
return;
|
||||
}
|
||||
_lastInferenceAt = now;
|
||||
_processingFrame = true;
|
||||
unawaited(_runYolo(image).whenComplete(() => _processingFrame = false));
|
||||
}
|
||||
|
||||
Future<void> _runYolo(CameraImage image) async {
|
||||
final detection = await sl<YoloDetector>().detect(image);
|
||||
if (detection == null || !mounted) return;
|
||||
await _handleDetection(detection);
|
||||
}
|
||||
|
||||
Future<void> _simulateObstacle() async {
|
||||
final detection = await sl<YoloDetector>().detectFallback();
|
||||
if (detection == null) return;
|
||||
await _handleDetection(detection, forceAlert: true);
|
||||
}
|
||||
|
||||
Future<void> _handleDetection(
|
||||
DetectionResult detection, {
|
||||
bool forceAlert = false,
|
||||
}) async {
|
||||
_lastDetection = detection;
|
||||
await sl<ApiClient>().dio.post('/user/obstacle', data: {
|
||||
'label': detection.label,
|
||||
'confidence': detection.confidence,
|
||||
'direction': detection.directionName,
|
||||
'estimatedDist': detection.estimatedDistance,
|
||||
'lat': null,
|
||||
'lng': null,
|
||||
});
|
||||
await sl<HapticService>().obstacleClose();
|
||||
await sl<TtsService>().speakImmediate(detection.spokenId);
|
||||
setState(() => _status =
|
||||
'Obstacle: ${detection.label} ${detection.directionName} ${detection.estimatedDistance}');
|
||||
|
||||
final now = DateTime.now();
|
||||
if (!forceAlert &&
|
||||
now.difference(_lastAlertAt) < const Duration(seconds: 4)) {
|
||||
return;
|
||||
}
|
||||
_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);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -235,12 +295,8 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
|
||||
String label = 'person';
|
||||
String direction = 'CENTER';
|
||||
String distance = 'Demo';
|
||||
var modelLoaded = false;
|
||||
try {
|
||||
await rootBundle.load(_selectedModel).timeout(const Duration(seconds: 3));
|
||||
modelLoaded = true;
|
||||
} catch (_) {}
|
||||
final detection = await sl<YoloDetector>().detectFallback();
|
||||
final modelLoaded = sl<YoloDetector>().isReady;
|
||||
final detection = await sl<YoloDetector>().detectSynthetic();
|
||||
if (detection != null) {
|
||||
label = detection.label;
|
||||
direction = detection.directionName;
|
||||
@@ -507,9 +563,7 @@ class _EmptyPanel extends StatelessWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
const _EmptyPanel(
|
||||
{required this.icon,
|
||||
required this.title,
|
||||
required this.message});
|
||||
{required this.icon, required this.title, required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user