import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:screenshot/screenshot.dart'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_mjpeg/flutter_mjpeg.dart'; import 'package:path_provider/path_provider.dart'; import 'package:image/image.dart' as img; import 'yolo_detector.dart'; import 'preview_screen.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import '../utils/translations.dart'; enum ImageFilterType { none, greyscale, invert} class CameraStreamScreen extends StatefulWidget { final int cameraIndex; const CameraStreamScreen({super.key, required this.cameraIndex}); @override State createState() => _CameraStreamScreenState(); } class _CameraStreamScreenState extends State with SingleTickerProviderStateMixin { String? _streamUrl; bool _isError = false; bool _isCapturing = false; final ScreenshotController _screenshotController = ScreenshotController(); final TransformationController _transformationController = TransformationController(); ImageFilterType _selectedFilter = ImageFilterType.none; late YOLODetector _yoloDetector; int _numberOfBullets = 5; // Brightness and contrast controls double _brightness = 0.0; // -1.0 to 1.0 double _contrast = 1.0; // 0.0 to 2.0 late AnimationController _zoomAnimController; Animation? _zoomAnimation; Matrix4? _originalMatrix; Color _bulletColor = Colors.blue; // default/fallback bullet color String _selectedLanguage = 'English'; String _t(String key) { return AppTranslations.translate(key, _selectedLanguage); } @override void initState() { super.initState(); _loadLanguage(); _loadStreamUrl(); _loadBulletCount(); _loadSelectedFilter(); _loadBulletColor(); // 👈 add this to actually load the saved bullet color _loadBrightnessContrast(); YOLODetector.create().then((detector) => _yoloDetector = detector); _setSystemBarStyle(); // 👈 set colors SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // 👈 show bars _keepScreenAwake(); _zoomAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); } Future _loadBrightnessContrast() async { final prefs = await SharedPreferences.getInstance(); setState(() { _brightness = prefs.getDouble('brightness') ?? 0.0; _contrast = prefs.getDouble('contrast') ?? 1.0; }); } Future _saveBrightnessContrast() async { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble('brightness', _brightness); await prefs.setDouble('contrast', _contrast); } Future _loadLanguage() async { final prefs = await SharedPreferences.getInstance(); setState(() { _selectedLanguage = prefs.getString('language') ?? 'English'; }); } Future _loadBulletColor() async { final prefs = await SharedPreferences.getInstance(); final selected = prefs.getString('bulletColor') ?? 'Blue'; setState(() { _bulletColor = _getColorFromName(selected); }); } Color _getColorFromName(String colorName) { switch (colorName.toLowerCase()) { case 'blue': return Colors.blue; case 'green': return Colors.green; case 'yellow': return Colors.yellow; case 'purple': return Colors.purple; case 'cyan': return Colors.cyan; case 'teal': return Colors.teal; case 'indigo': return Colors.indigo; case 'pink': return Colors.pink; case 'lime': return Colors.lime; case 'mint': return Colors.greenAccent; case 'sky': return Colors.orange; case 'violet': return Colors.deepPurpleAccent; case 'amber': return Colors.amber; case 'brown': return Colors.brown; case 'grey': return Colors.grey; default: return Colors.blue; // fallback } } void _setSystemBarStyle() { SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( statusBarColor: Colors.grey[900], // ✅ match your AppBar statusBarIconBrightness: Brightness.light, // white icons systemNavigationBarColor: Colors.grey[900], systemNavigationBarIconBrightness: Brightness.light, ), ); } Future _loadSelectedFilter() async { final prefs = await SharedPreferences.getInstance(); final index = prefs.getInt('selectedFilter'); if (index != null && index >= 0 && index < ImageFilterType.values.length) { setState(() { _selectedFilter = ImageFilterType.values[index]; }); } } Future _keepScreenAwake() async { try { await WakelockPlus.enable(); } catch (e) { debugPrint("⚠️ Could not keep screen awake: $e"); } } Future _loadStreamUrl() async { final prefs = await SharedPreferences.getInstance(); String? url = prefs.getString('cameraIP_${widget.cameraIndex}'); setState(() { _streamUrl = (url != null && url.trim().isNotEmpty) ? (url.startsWith(RegExp(r"https?://")) ? url : "http://$url") : null; _isError = _streamUrl == null; }); } Future _loadBulletCount() async { final prefs = await SharedPreferences.getInstance(); setState(() { _numberOfBullets = prefs.getInt('bulletCount') ?? 5; }); } Future _captureAndPreviewImage() async { if (_streamUrl == null || _isCapturing) return; setState(() => _isCapturing = true); _originalMatrix = _transformationController.value; try { final targetMatrix = Matrix4.identity()..scale(1.0); final isZoomedIn = !_matrixEquals(_originalMatrix!, targetMatrix); if (isZoomedIn) { _zoomAnimController.reset(); _zoomAnimation = Matrix4Tween( begin: _originalMatrix, end: targetMatrix, ).animate(CurvedAnimation( parent: _zoomAnimController, curve: Curves.easeOut, )); void updateZoom() { _transformationController.value = _zoomAnimation!.value; } _zoomAnimController.addListener(updateZoom); await _zoomAnimController.forward(); _zoomAnimController.removeListener(updateZoom); } await Future.delayed(const Duration(milliseconds: 100)); final imageBytes = await _screenshotController.capture(); if (imageBytes == null || imageBytes.isEmpty) return; final decoded = img.decodeImage(imageBytes); if (decoded == null) return; final width = decoded.width; final height = decoded.height; final size = width < height ? width : height; final offsetX = (width - size) ~/ 2; final offsetY = (height - size) ~/ 2; final cropped = img.copyCrop(decoded, x: offsetX, y: offsetY, width: size, height: size); // Debugging: log the selected filter debugPrint("📸 Selected filter: $_selectedFilter"); // Create the output directory and path final dir = await getApplicationDocumentsDirectory(); final path = '${dir.path}/ip_capture_temp.jpg'; img.Image processed = cropped; // Apply the selected filter switch (_selectedFilter) { case ImageFilterType.greyscale: processed = img.grayscale(cropped); break; case ImageFilterType.invert: processed = invertImage(cropped); // ✅ use your custom invert function break; case ImageFilterType.none: break; } // Save the processed image to a file final file = File(path)..writeAsBytesSync(img.encodeJpg(processed)); // Detect objects in the image using YOLO final detections = await _yoloDetector.detect(file); if (!mounted) return; // Navigate to the preview screen Navigator.push( context, MaterialPageRoute( builder: (_) => PreviewScreen( imagePath: file.path, detections: detections, bulletCount: _numberOfBullets, ), ), ); } catch (e) { debugPrint("❌ Capture error: $e"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Capture failed: $e")), ); } } finally { _zoomAnimController.reset(); _zoomAnimation = Matrix4Tween( begin: _transformationController.value, end: _originalMatrix!, ).animate(CurvedAnimation( parent: _zoomAnimController, curve: Curves.easeIn, )); void restoreZoom() { _transformationController.value = _zoomAnimation!.value; } _zoomAnimController.addListener(restoreZoom); await _zoomAnimController.forward(); _zoomAnimController.removeListener(restoreZoom); setState(() => _isCapturing = false); } } img.Image invertImage(img.Image source) { final result = img.Image.from(source); for (int y = 0; y < result.height; y++) { for (int x = 0; x < result.width; x++) { final pixel = result.getPixel(x, y); result.setPixelRgb(x, y, 255 - pixel.r, 255 - pixel.g, 255 - pixel.b); } } return result; } bool _matrixEquals(Matrix4 a, Matrix4 b) { for (int i = 0; i < 16; i++) { if ((a.storage[i] - b.storage[i]).abs() > 0.001) return false; } return true; } void _showBulletDialog() { showDialog( context: context, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24.0), // Increased padding for better spacing child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Text( _t('Select the number of bullets'), textAlign: TextAlign.center, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), Center( child: Wrap( alignment: WrapAlignment.center, spacing: 12, runSpacing: 12, children: List.generate(5, (index) { final bulletNum = index + 1; final isSelected = _numberOfBullets == bulletNum; return ElevatedButton( onPressed: () { _selectBulletCount(bulletNum); Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( backgroundColor: isSelected ? _bulletColor : Colors.grey[800], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( '$bulletNum', style: TextStyle( fontSize: 18, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ); }), ), ), ], ), ), ); }, ); } void _showFilterDialog() { showDialog( context: context, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _t('Choose a Filter'), style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Wrap( spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, children: ImageFilterType.values.map((f) { final isSelected = _selectedFilter == f; final label = f.toString().split('.').last; final translatedLabel = _t(label[0].toUpperCase() + label.substring(1)); return ElevatedButton( onPressed: () async { setState(() { _selectedFilter = f; }); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('selectedFilter', f.index); // Save filter index if (context.mounted) { Navigator.of(context).pop(); } }, style: ElevatedButton.styleFrom( backgroundColor: isSelected ? _bulletColor : Colors.grey[800], foregroundColor: Colors.white, ), child: Text(translatedLabel), ); }).toList(), ), ], ), ), ); }, ); } void _selectBulletCount(int number) async { final prefs = await SharedPreferences.getInstance(); setState(() { _numberOfBullets = number; }); await prefs.setInt('bulletCount', number); } void _showHelpDialog() { showDialog( context: context, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _t('Help'), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), _buildHelpItem(Icons.camera_alt, _t('Tap camera button to capture and detect bullets')), const SizedBox(height: 12), _buildHelpItem(Icons.filter_9_plus, _t('Bottom left number: Set expected bullet count (1-5)')), const SizedBox(height: 12), _buildHelpItem(Icons.brightness_6, _t('Sun icon: Adjust brightness and contrast for better visibility')), const SizedBox(height: 12), _buildHelpItem(Icons.filter_alt, _t('Filter icon: Apply color filters (None, Greyscale, Invert)')), const SizedBox(height: 12), _buildHelpItem(Icons.pinch, _t('Pinch to zoom on the camera stream')), const SizedBox(height: 20), Center( child: ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, foregroundColor: Colors.white, ), child: Text(_t('Got it!')), ), ), ], ), ), ); }, ); } Widget _buildHelpItem(IconData icon, String text) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: _bulletColor, size: 20), const SizedBox(width: 12), Expanded( child: Text( text, style: const TextStyle(color: Colors.white, fontSize: 14), ), ), ], ); } void _showBrightnessContrastDialog() { showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setDialogState) { return Dialog( backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _t('Brightness & Contrast'), style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 24), // Brightness slider Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_t('Brightness')}: ${(_brightness * 100).toInt()}', style: const TextStyle(color: Colors.white, fontSize: 16), ), Slider( value: _brightness, min: -1.0, max: 1.0, divisions: 40, activeColor: _bulletColor, onChanged: (value) { setState(() { _brightness = value; }); setDialogState(() {}); _saveBrightnessContrast(); }, ), ], ), const SizedBox(height: 16), // Contrast slider Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_t('Contrast')}: ${(_contrast * 100).toInt()}%', style: const TextStyle(color: Colors.white, fontSize: 16), ), Slider( value: _contrast, min: 0.5, max: 2.0, divisions: 30, activeColor: _bulletColor, onChanged: (value) { setState(() { _contrast = value; }); setDialogState(() {}); _saveBrightnessContrast(); }, ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { setState(() { _brightness = 0.0; _contrast = 1.0; }); setDialogState(() {}); _saveBrightnessContrast(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[800], foregroundColor: Colors.white, ), child: Text(_t('Reset')), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, foregroundColor: Colors.white, ), child: Text(_t('Done')), ), ], ), ], ), ), ); }, ); }, ); } @override void dispose() { _zoomAnimController.dispose(); _yoloDetector.close(); _restoreScreenSettings(); _setSystemBarStyle(); // restore grey[900] on back super.dispose(); } Future _restoreScreenSettings() async { try { await WakelockPlus.disable(); } catch (e) { debugPrint("⚠️ Could not restore screen settings: $e"); } } ColorFilter _buildColorFilter() { // Base filter matrix List baseMatrix; switch (_selectedFilter) { case ImageFilterType.greyscale: baseMatrix = [ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]; break; case ImageFilterType.invert: baseMatrix = [ -1.0, 0.0, 0.0, 0.0, 255.0, 0.0, -1.0, 0.0, 0.0, 255.0, 0.0, 0.0, -1.0, 0.0, 255.0, 0.0, 0.0, 0.0, 1.0, 0.0, ]; break; default: baseMatrix = [ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ]; } // Apply brightness and contrast adjustments final b = _brightness * 255.0; // Convert to 0-255 range final c = _contrast; // Combine filter with brightness/contrast final adjustedMatrix = [ c, 0.0, 0.0, 0.0, b, 0.0, c, 0.0, 0.0, b, 0.0, 0.0, c, 0.0, b, 0.0, 0.0, 0.0, 1.0, 0.0, ]; // Multiply base matrix with adjustment matrix return ColorFilter.matrix(_multiplyMatrices(baseMatrix, adjustedMatrix)); } List _multiplyMatrices(List a, List b) { // For simplicity, just apply brightness/contrast on top of base filter final result = []; for (int i = 0; i < a.length; i++) { result.add(a[i]); } // Apply contrast to RGB components result[0] = result[0] * b[0]; // R result[6] = result[6] * b[6]; // G result[12] = result[12] * b[12]; // B // Apply brightness result[4] = result[4] + b[4]; // R offset result[9] = result[9] + b[9]; // G offset result[14] = result[14] + b[14]; // B offset return result; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("${_t('Camera')} ${widget.cameraIndex}"), backgroundColor: Colors.grey[900], foregroundColor: Colors.white, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), actions: [ IconButton( icon: const Icon(Icons.help_outline, color: Colors.white), onPressed: _showHelpDialog, ), const SizedBox(width: 16), ], ), backgroundColor: Colors.black, body: Stack( children: [ Screenshot( controller: _screenshotController, child: LayoutBuilder( builder: (context, constraints) { return Center( child: (_isError || _streamUrl == null) ? _buildErrorContent() : InteractiveViewer( clipBehavior: Clip.hardEdge, constrained: false, boundaryMargin: EdgeInsets.zero, minScale: 1.0, maxScale: 5.0, transformationController: _transformationController, child: SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: RepaintBoundary( child: ColorFiltered( colorFilter: _buildColorFilter(), child: Mjpeg( stream: _streamUrl!, isLive: true, fit: BoxFit.contain, error: (context, error, stackTrace) { return _buildErrorContent(); }, ), ), ), ), ), ); }, ), ), if (_isCapturing) Positioned.fill( child: Container( color: Colors.black.withValues(alpha: 0.4), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: Colors.white), const SizedBox(height: 16), Text( _t("Processing image..."), style: const TextStyle(color: Colors.white, fontSize: 16), ), ], ), ), ), ], ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: _isError ? null : FloatingActionButton( backgroundColor: Colors.white, onPressed: _captureAndPreviewImage, child: const Icon(Icons.camera_alt, color: Colors.black), ), bottomNavigationBar: BottomAppBar( color: Colors.grey[900], shape: const CircularNotchedRectangle(), notchMargin: 8.0, child: SizedBox( height: 60, child: Row( children: [ // Left - Bullet count (just number) Expanded( child: Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(left: 16.0), child: InkWell( onTap: _showBulletDialog, child: Text( '$_numberOfBullets', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ), ), // Center space for FAB const SizedBox(width: 80), // Right section - Brightness and Filter Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( icon: const Icon(Icons.brightness_6, color: Colors.white, size: 24), onPressed: _showBrightnessContrastDialog, ), IconButton( icon: const Icon(Icons.filter_alt, color: Colors.white, size: 24), onPressed: _showFilterDialog, ), ], ), ), ], ), ), ), ); } Widget _buildErrorContent() { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, color: Colors.red, size: 100), const SizedBox(height: 20), Text( _t("Unable to load camera stream"), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), ElevatedButton( onPressed: _loadStreamUrl, child: Text(_t("Retry")), ), ], ); } }