import 'package:flutter/material.dart'; import 'package:flutter_mjpeg/flutter_mjpeg.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/translations.dart'; class SpectatorScreen extends StatefulWidget { final String selectedLanguage; const SpectatorScreen({ super.key, required this.selectedLanguage, }); @override State createState() => _SpectatorScreenState(); } class _SpectatorScreenState extends State { List _selectedCameras = List.generate(6, (_) => true); // All selected by default List _enabledCameras = List.generate(6, (_) => false); // Which cameras have IPs configured List> _cameras = []; Color _bulletColor = Colors.blue; // default/fallback bullet color @override void initState() { super.initState(); _loadCameras(); _loadBulletColor(); _setSystemUIMode(); } @override void dispose() { super.dispose(); } void _setSystemUIMode() { // No special system UI mode needed for portrait } String _t(String key) { return AppTranslations.translate(key, widget.selectedLanguage); } Future _loadCameras() async { final prefs = await SharedPreferences.getInstance(); List> cameras = []; List enabledCameras = List.generate(6, (_) => false); for (int i = 0; i < 6; i++) { String? ip = prefs.getString('cameraIP_${i + 1}'); if (ip != null && ip.isNotEmpty && ip != 'Not Set') { cameras.add({ 'index': i + 1, 'ip': ip, }); enabledCameras[i] = true; // Mark camera as enabled } } if (mounted) { setState(() { _cameras = cameras; _enabledCameras = enabledCameras; }); } } 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 'orange': return Colors.orange; case 'red': return Colors.red; default: return Colors.blue; } } void _showCameraSelection() { showModalBottomSheet( context: context, backgroundColor: Colors.grey[900], shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) => StatefulBuilder( builder: (context, setModalState) => SingleChildScrollView( child: Container( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _t('Select Cameras'), style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), // Single row of 6 camera buttons Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(6, (index) { final cameraNum = index + 1; final isSelected = _selectedCameras[index]; final isEnabled = _enabledCameras[index]; return GestureDetector( onTap: isEnabled ? () { setModalState(() { _selectedCameras[index] = !_selectedCameras[index]; }); // Also update the parent state to refresh camera grid if (mounted) { setState(() {}); } } : null, child: Container( width: 50, height: 50, decoration: BoxDecoration( color: !isEnabled ? Colors.grey[700] : (isSelected ? _bulletColor : Colors.grey[800]), borderRadius: BorderRadius.circular(8), border: Border.all( color: !isEnabled ? Colors.grey[600]! : (isSelected ? _bulletColor : Colors.grey[700]!), width: 2, ), ), child: Center( child: Text( '$cameraNum', style: TextStyle( color: isEnabled ? Colors.white : Colors.grey[500], fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ); }), ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 40, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[800], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), onPressed: () => Navigator.pop(context), child: Text( _t('Done'), style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ), ), ); } @override Widget build(BuildContext context) { final visibleCameras = >[]; for (int i = 0; i < _cameras.length; i++) { if (_selectedCameras[_cameras[i]['index'] - 1]) { visibleCameras.add(_cameras[i]); } } return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: Text( _t('Spectate'), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600, ), ), backgroundColor: Colors.grey[900], elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), actions: [ IconButton( icon: const Icon(Icons.settings, color: Colors.white), onPressed: _showCameraSelection, ), ], ), body: visibleCameras.isEmpty ? Center( child: Text( _t('No cameras selected'), style: const TextStyle( color: Colors.white, fontSize: 16, ), ), ) : ListView.builder( padding: const EdgeInsets.all(8), itemCount: visibleCameras.length, itemBuilder: (context, index) { final camera = visibleCameras[index]; final cameraIndex = camera['index'] as int; final cameraIp = camera['ip'] as String; final streamUrl = cameraIp.startsWith(RegExp(r"https?://")) ? cameraIp : "http://$cameraIp"; // 16:9 aspect ratio = width / height return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[800]!, width: 1), ), child: AspectRatio( aspectRatio: 16 / 9, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Stack( children: [ Mjpeg( stream: streamUrl, isLive: true, fit: BoxFit.contain, error: (context, error, stackTrace) { return Container( color: Colors.grey[700], child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.videocam_off, color: Colors.white, size: 32, ), const SizedBox(height: 8), Text( "${_t('Camera')} $cameraIndex", style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), ], ), ), ); }, ), // Camera number circle - top right (with bullet color) Positioned( top: 8, right: 8, child: Container( width: 36, height: 36, decoration: BoxDecoration( color: _bulletColor, shape: BoxShape.circle, ), child: Center( child: Text( '$cameraIndex', style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ), ], ), ), ), ); }, ), ); } }