diff --git a/lib/screens/book_screen.dart b/lib/screens/book_screen.dart index 4a4eee9..7b00b20 100644 --- a/lib/screens/book_screen.dart +++ b/lib/screens/book_screen.dart @@ -376,13 +376,24 @@ Future _renameFolder(String oldFolderName) async { ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Center( - child: Text( - isTournament ? _t('Tournament Mode 🏆') : _t('Mark as Tournament 🏆'), - style: TextStyle( - color: isTournament ? _bulletColor : Colors.white70, - fontWeight: FontWeight.w500, - fontSize: 16, - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.emoji_events, + size: 16, + color: isTournament ? _bulletColor : Colors.white70, + ), + const SizedBox(width: 8), + Text( + isTournament ? _t('Tournament Mode') : _t('Mark as Tournament'), + style: TextStyle( + color: isTournament ? _bulletColor : Colors.white70, + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + ], ), ), ), @@ -594,14 +605,14 @@ Widget _buildFolderRow(String folderName) { : 'Unknown Date'; return Container( - margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(12), ), child: Column( children: [ - // Header section: Name and Date + // Header section with grey background like toolbars InkWell( onTap: () { setState(() { @@ -613,40 +624,56 @@ Widget _buildFolderRow(String folderName) { topRight: Radius.circular(12), ), child: Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - // Small colored indicator + // White indicator line Container( width: 4, height: 20, decoration: BoxDecoration( - color: _bulletColor, + color: Colors.white, borderRadius: BorderRadius.circular(2), ), ), - const SizedBox(width: 8), - Text( - _folderTypes[folderName] == true - ? '🏆 $folderName' - : folderName, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _bulletColor), + const SizedBox(width: 12), + Row( + children: [ + if (_folderTypes[folderName] == true) ...[ + const Icon(Icons.emoji_events, size: 18, color: Colors.white), + const SizedBox(width: 8), + ], + Text( + folderName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), + ), + ], ), ], ), Row( + mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.calendar_today, size: 14, color: Colors.white), + const SizedBox(width: 6), Text( - "📅 $formattedDate", - style: const TextStyle(fontSize: 13, color: Colors.black87), + formattedDate, + style: const TextStyle(fontSize: 13, color: Colors.white), ), - const SizedBox(width: 8), + const SizedBox(width: 12), Icon( _expandedFolder == folderName ? Icons.expand_less : Icons.expand_more, - color: Colors.black87, + color: Colors.white, ), ], ), @@ -698,7 +725,7 @@ Widget _buildFolderRow(String folderName) { Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.emoji_events, size: 16, color: Colors.amber), + const Icon(Icons.emoji_events, size: 16, color: Colors.black87), const SizedBox(width: 4), Text( "$totalScore", @@ -816,7 +843,7 @@ Widget _buildTargetsList(String folderName) { const SizedBox(width: 8), const Icon( Icons.emoji_events, - color: Colors.amber, + color: Colors.black87, size: 14, ), const SizedBox(width: 4), @@ -893,13 +920,28 @@ Widget build(BuildContext context) { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - _folderFilter == 'all' - ? _t('Showing: 📂 All folders') - : _folderFilter == 'tournament' - ? _t('Showing: 🏆 Tournament folders') - : _t('Showing: 🎯 Training folders'), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _folderFilter == 'all' + ? Icons.folder + : _folderFilter == 'tournament' + ? Icons.emoji_events + : Icons.track_changes, + size: 16, + color: Colors.black, + ), + const SizedBox(width: 6), + Text( + _folderFilter == 'all' + ? _t('Showing: All folders') + : _folderFilter == 'tournament' + ? _t('Showing: Tournament folders') + : _t('Showing: Training folders'), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black), + ), + ], ), Icon(Icons.filter_list, color: Colors.black87, size: 18), ], @@ -917,6 +959,7 @@ Widget build(BuildContext context) { thickness: 6, // optional: adjust width radius: const Radius.circular(8), child: ListView.builder( + padding: const EdgeInsets.only(top: 8), itemCount: _filteredFolderNames().length, itemBuilder: (context, index) { String folderName = _filteredFolderNames()[index]; @@ -955,11 +998,11 @@ void _showFolderFilterPopup(BuildContext context) { const SizedBox(height: 24), // Folder type filters - _buildStyledFilterButton('all', _t('📂 All folders'), context), + _buildStyledFilterButton('all', Icons.folder, _t('All folders'), context), const SizedBox(height: 12), - _buildStyledFilterButton('tournament', _t('🏆 Tournament'), context), + _buildStyledFilterButton('tournament', Icons.emoji_events, _t('Tournament'), context), const SizedBox(height: 12), - _buildStyledFilterButton('training', _t('🎯 Training'), context), + _buildStyledFilterButton('training', Icons.track_changes, _t('Training'), context), const SizedBox(height: 24), @@ -986,16 +1029,27 @@ void _showFolderFilterPopup(BuildContext context) { borderRadius: BorderRadius.circular(12), ), child: Center( - child: Text( - _t('📅 Sort by Date'), - textAlign: TextAlign.center, - style: TextStyle( - color: _sortMode == 'date' ? Colors.white : Colors.white70, - fontWeight: FontWeight.w500, - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: _sortMode == 'date' ? Colors.white : Colors.white70, + ), + const SizedBox(width: 4), + Text( + _t('Sort by Date'), + textAlign: TextAlign.center, + style: TextStyle( + color: _sortMode == 'date' ? Colors.white : Colors.white70, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], ), ), ), @@ -1015,16 +1069,27 @@ void _showFolderFilterPopup(BuildContext context) { borderRadius: BorderRadius.circular(12), ), child: Center( - child: Text( - _t('🏆 Sort by Score'), - textAlign: TextAlign.center, - style: TextStyle( - color: _sortMode == 'score' ? Colors.white : Colors.white70, - fontWeight: FontWeight.w500, - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.emoji_events, + size: 14, + color: _sortMode == 'score' ? Colors.white : Colors.white70, + ), + const SizedBox(width: 4), + Text( + _t('Sort by Score'), + textAlign: TextAlign.center, + style: TextStyle( + color: _sortMode == 'score' ? Colors.white : Colors.white70, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], ), ), ), @@ -1043,7 +1108,7 @@ void _showFolderFilterPopup(BuildContext context) { } -Widget _buildStyledFilterButton(String value, String label, BuildContext context) { +Widget _buildStyledFilterButton(String value, IconData icon, String label, BuildContext context) { final isSelected = _folderFilter == value; return SizedBox( @@ -1062,13 +1127,24 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context borderRadius: BorderRadius.circular(12), ), child: Center( - child: Text( - label, - style: TextStyle( - color: isSelected ? Colors.white : Colors.white70, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 18, + color: isSelected ? Colors.white : Colors.white70, + ), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.white70, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), ), diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 47a9676..e50f6aa 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -473,15 +473,15 @@ class _CalendarScreenState extends State { onTap: () => _makePhoneCall(contact.phone), onLongPress: () => _showContactPopup(context, contact), child: Container( - margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: Colors.grey[300], - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.grey.withValues(alpha: 0.08), - blurRadius: 2, - offset: const Offset(0, 1), + color: Colors.grey.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), ), ], ), @@ -490,37 +490,37 @@ class _CalendarScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Colored header bar (like event cards) + // Grey header bar with white text (matching book screen style) Container( width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.2), + color: Colors.grey[900], borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), child: Row( children: [ - // Small colored indicator + // White indicator line Container( - width: 3, - height: 12, + width: 4, + height: 20, decoration: BoxDecoration( - color: accentColor, + color: Colors.white, borderRadius: BorderRadius.circular(2), ), ), - const SizedBox(width: 7), - // Contact name + const SizedBox(width: 12), + // Contact name in white Expanded( child: Text( _capitalize(contact.fullName), - style: TextStyle( + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: accentColor, + color: Colors.white, ), ), ), @@ -1154,9 +1154,7 @@ class _CalendarScreenState extends State { width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: isToday - ? cardAccentColor.withValues(alpha: 0.2) - : cardAccentColor.withValues(alpha: 0.1), + color: cardAccentColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), @@ -1164,22 +1162,22 @@ class _CalendarScreenState extends State { ), child: Row( children: [ - // Small colored indicator + // Small white indicator line Container( width: 4, height: 16, decoration: BoxDecoration( - color: cardAccentColor, + color: Colors.white, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( dateText, - style: TextStyle( + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: cardAccentColor, + color: Colors.white, ), ), const Spacer(), diff --git a/lib/screens/camera_selection_dialog.dart b/lib/screens/camera_selection_dialog.dart new file mode 100644 index 0000000..9c24e1c --- /dev/null +++ b/lib/screens/camera_selection_dialog.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import '../utils/translations.dart'; + +class CameraSelectionDialog extends StatefulWidget { + final List> availableCameras; + final String selectedLanguage; + + const CameraSelectionDialog({ + super.key, + required this.availableCameras, + required this.selectedLanguage, + }); + + @override + State createState() => _CameraSelectionDialogState(); +} + +class _CameraSelectionDialogState extends State { + late List _selectedCameras; + + @override + void initState() { + super.initState(); + _selectedCameras = List.generate(widget.availableCameras.length, (_) => false); + } + + String _t(String key) { + return AppTranslations.translate(key, widget.selectedLanguage); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: Text( + _t('Select Cameras to Spectate'), + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _t('Choose 1-6 cameras'), + style: TextStyle( + color: Colors.grey[400], + fontSize: 14, + ), + ), + const SizedBox(height: 16), + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.availableCameras.length, + itemBuilder: (context, index) { + final camera = widget.availableCameras[index]; + final cameraIndex = camera['index'] as int; + final cameraIp = camera['ip'] as String; + + return CheckboxListTile( + title: Text( + "${_t('Camera')} $cameraIndex", + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + cameraIp, + style: TextStyle( + color: Colors.grey[400], + fontSize: 12, + ), + ), + value: _selectedCameras[index], + onChanged: (value) { + setState(() { + _selectedCameras[index] = value ?? false; + }); + }, + activeColor: Colors.grey[800], + checkColor: Colors.white, + tileColor: Colors.grey[850], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + _t('Cancel'), + style: const TextStyle(color: Colors.grey), + ), + ), + TextButton( + onPressed: _selectedCameras.contains(true) + ? () { + final selectedCameras = >[]; + for (int i = 0; i < _selectedCameras.length; i++) { + if (_selectedCameras[i]) { + selectedCameras.add(widget.availableCameras[i]); + } + } + Navigator.pop(context, selectedCameras); + } + : null, + child: Text( + _t('View Cameras'), + style: TextStyle( + color: _selectedCameras.contains(true) + ? Colors.white + : Colors.grey[600], + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/multi_camera_screen.dart b/lib/screens/multi_camera_screen.dart new file mode 100644 index 0000000..eeecbfb --- /dev/null +++ b/lib/screens/multi_camera_screen.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mjpeg/flutter_mjpeg.dart'; +import '../utils/translations.dart'; + +class MultiCameraScreen extends StatefulWidget { + final List> cameras; + final String selectedLanguage; + + const MultiCameraScreen({ + super.key, + required this.cameras, + required this.selectedLanguage, + }); + + @override + State createState() => _MultiCameraScreenState(); +} + +class _MultiCameraScreenState extends State { + int? _fullscreenIndex; // null = grid view, index = fullscreen camera + + String _t(String key) { + return AppTranslations.translate(key, widget.selectedLanguage); + } + + // Calculate grid columns based on camera count + int get _crossAxisCount { + final count = widget.cameras.length; + if (count == 1) return 1; + if (count <= 4) return 2; + return 3; // 5-6 cameras + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + _fullscreenIndex == null + ? _t('Multi-Camera View') + : "${_t('Camera')} ${widget.cameras[_fullscreenIndex!]['index']}", + ), + backgroundColor: Colors.grey[900], + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () { + if (_fullscreenIndex != null) { + setState(() { + _fullscreenIndex = null; // Exit fullscreen first + }); + } else { + Navigator.pop(context); // Exit screen + } + }, + ), + ), + backgroundColor: Colors.black, + body: _fullscreenIndex == null + ? _buildGridView() + : _buildFullscreenView(_fullscreenIndex!), + ); + } + + Widget _buildGridView() { + return GridView.builder( + padding: const EdgeInsets.all(8), + itemCount: widget.cameras.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _crossAxisCount, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 4 / 3, // 4:3 aspect ratio for camera feeds + ), + itemBuilder: (context, index) { + return _buildCameraCell(index); + }, + ); + } + + Widget _buildCameraCell(int index) { + final camera = widget.cameras[index]; + final cameraIndex = camera['index'] as int; + final cameraIp = camera['ip'] as String; + + final streamUrl = cameraIp.startsWith(RegExp(r"https?://")) + ? cameraIp + : "http://$cameraIp"; + + return GestureDetector( + onTap: () { + setState(() { + _fullscreenIndex = index; // Enter fullscreen + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[700]!, width: 1), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + // MJPEG Stream + Mjpeg( + stream: streamUrl, + isLive: true, + fit: BoxFit.contain, + error: (context, error, stackTrace) { + return _buildErrorWidget(cameraIndex); + }, + ), + // Camera label overlay + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "${_t('Camera')} $cameraIndex", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildFullscreenView(int index) { + final camera = widget.cameras[index]; + final cameraIndex = camera['index'] as int; + final cameraIp = camera['ip'] as String; + + final streamUrl = cameraIp.startsWith(RegExp(r"https?://")) + ? cameraIp + : "http://$cameraIp"; + + return GestureDetector( + onTap: () { + setState(() { + _fullscreenIndex = null; // Exit fullscreen, return to grid + }); + }, + child: Container( + color: Colors.black, + child: Stack( + children: [ + Center( + child: Mjpeg( + stream: streamUrl, + isLive: true, + fit: BoxFit.contain, + error: (context, error, stackTrace) { + return _buildErrorWidget(cameraIndex); + }, + ), + ), + // Back indicator overlay + Positioned( + bottom: 16, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.touch_app, color: Colors.white, size: 16), + const SizedBox(width: 8), + Text( + _t('Tap to return to grid'), + style: + const TextStyle(color: Colors.white, fontSize: 14), + ), + ], + ), + ), + ), + ), + // Camera label + Positioned( + top: 16, + left: 16, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + "${_t('Camera')} $cameraIndex", + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildErrorWidget(int cameraIndex) { + return Container( + color: Colors.black, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.videocam_off, color: Colors.red, size: 32), + const SizedBox(height: 4), + Flexible( + child: Text( + "${_t('Camera')} $cameraIndex\n${_t('Stream unavailable')}", + style: const TextStyle( + color: Colors.white70, + fontSize: 11, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/preview_screen.dart b/lib/screens/preview_screen.dart index 0f84ac6..f771814 100644 --- a/lib/screens/preview_screen.dart +++ b/lib/screens/preview_screen.dart @@ -576,9 +576,17 @@ void _moveBullet(Offset offsetDelta) { ), child: Column( children: [ - Text( - '🎯 $nextTraining', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.track_changes, size: 18, color: Colors.white), + const SizedBox(width: 8), + Text( + nextTraining, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white), + ), + ], ), const SizedBox(height: 4), Text( @@ -611,9 +619,17 @@ void _moveBullet(Offset offsetDelta) { ), child: Column( children: [ - Text( - '🏆 $nextTournament', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.emoji_events, size: 18, color: Colors.white), + const SizedBox(width: 8), + Text( + nextTournament, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white), + ), + ], ), const SizedBox(height: 4), Text( @@ -656,9 +672,21 @@ void _moveBullet(Offset offsetDelta) { ), child: Column( children: [ - Text( - '${lastIsTournament ? "🏆" : "🎯"} $lastFolder', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + lastIsTournament ? Icons.emoji_events : Icons.track_changes, + size: 18, + color: Colors.white, + ), + const SizedBox(width: 8), + Text( + lastFolder, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white), + ), + ], ), const SizedBox(height: 4), Text( @@ -982,48 +1010,34 @@ Widget build(BuildContext context) { ), ], ), - child: Column( - children: [ - Container( - height: 30, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - alignment: Alignment.center, - child: Text( + child: Container( + decoration: BoxDecoration( + color: _getBulletButtonColor(index), + borderRadius: BorderRadius.circular(16), + ), + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( '${index + 1}', style: const TextStyle( fontWeight: FontWeight.bold, - fontSize: 14, - color: Colors.black87, + fontSize: 12, + color: Colors.white, ), ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: _getBulletButtonColor(index), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - ), - alignment: Alignment.center, - child: Text( - '$score', - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + const SizedBox(height: 4), + Text( + '$score', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), - ), - ], + ], + ), ), ), ), @@ -1095,41 +1109,36 @@ Widget build(BuildContext context) { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Left side - Label (darker grey) - const Padding( - padding: EdgeInsets.only(right: 16), + // Left side - Label with icon + Padding( + padding: const EdgeInsets.only(right: 16), child: FittedBox( fit: BoxFit.scaleDown, - child: Text( - "🎯 Total Score:", - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + child: Row( + children: [ + const Icon(Icons.track_changes, size: 24, color: Colors.white), + const SizedBox(width: 8), + const Text( + "Total Score:", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], ), ), ), - // Right side - Score badge (grey with rounded corners) - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.grey[800], - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.grey[700]!, - width: 2, - ), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "$_totalScore", - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + // Right side - Score number (no box) + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "$_totalScore", + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 8b3595b..df82dea 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -331,36 +331,169 @@ Future _saveLanguage(String language) async { orElse: () => _languages[0], ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _t("Language"), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black87, + return Container( + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), ), - ), - const SizedBox(height: 8), - GestureDetector( - onTap: _showLanguagePopup, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(12), + color: Colors.grey[900], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), ), child: Row( children: [ - Text( - selectedLang['flag'] ?? '🌐', - style: const TextStyle(fontSize: 20), + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2), + ), ), const SizedBox(width: 12), + Text( + _t("Language"), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + // Content + GestureDetector( + onTap: _showLanguagePopup, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + Text( + selectedLang['flag'] ?? '🌐', + style: const TextStyle(fontSize: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _t(selectedLang['name'] ?? 'English'), + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ), + const Icon(Icons.chevron_right, color: Colors.black54), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildBulletColorSelector() { + return Container( + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 12), + Text( + _t("Select Bullet Color"), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + // Content + GestureDetector( + onTap: _showBulletColorPopup, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + Container( + width: 16, + height: 16, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: _getColorFromName(_selectedBulletColor), + shape: BoxShape.circle, + border: Border.all(color: Colors.black26), + ), + ), Expanded( child: Text( - _t(selectedLang['name'] ?? 'English'), + _t(_selectedBulletColor), style: const TextStyle(color: Colors.black87, fontSize: 14), ), ), @@ -370,54 +503,7 @@ Future _saveLanguage(String language) async { ), ), ], - ); - } - - Widget _buildBulletColorSelector() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _t("Select Bullet Color"), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - const SizedBox(height: 8), - GestureDetector( - onTap: _showBulletColorPopup, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Container( - width: 16, - height: 16, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: _getColorFromName(_selectedBulletColor), - shape: BoxShape.circle, - border: Border.all(color: Colors.black26), - ), - ), - Expanded( - child: Text( - _t(_selectedBulletColor), - style: const TextStyle(color: Colors.black87, fontSize: 14), - ), - ), - const Icon(Icons.chevron_right, color: Colors.black54), - ], - ), - ), - ), - ], + ), ); } @@ -561,54 +647,113 @@ void _showBulletColorPopup() { } Widget _buildCameraList() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _t("Camera Settings"), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black87, + return Container( + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), ), - ), - const SizedBox(height: 8), - ...List.generate(6, (index) { - return GestureDetector( - onTap: () => _editCameraIP(index), - child: Container( - margin: const EdgeInsets.symmetric(vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${_t('Camera')} ${index + 1}", - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - Text( - _cameraIPs[index], - style: const TextStyle(color: Colors.black54), - ), - ], - ), - const Icon(Icons.chevron_right, color: Colors.black45), - ], + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), - ); - }), - ], + child: Row( + children: [ + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 12), + Text( + _t("Camera Settings"), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + // Content - all cameras + ...List.generate(6, (index) { + return GestureDetector( + onTap: () => _editCameraIP(index), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey[300], + border: index < 5 + ? Border( + bottom: BorderSide( + color: Colors.grey[400]!, + width: 1, + ), + ) + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${_t('Camera')} ${index + 1}", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + _cameraIPs[index], + style: const TextStyle(color: Colors.black54), + ), + ], + ), + const Icon(Icons.chevron_right, color: Colors.black54), + ], + ), + ), + if (index == 5) + Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ), + ], + ), + ); + }), + ], + ), ); } diff --git a/lib/screens/spectator_screen.dart b/lib/screens/spectator_screen.dart new file mode 100644 index 0000000..be98c5d --- /dev/null +++ b/lib/screens/spectator_screen.dart @@ -0,0 +1,342 @@ +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, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/stream_screen.dart b/lib/screens/stream_screen.dart index 36a7316..ffdb8ff 100644 --- a/lib/screens/stream_screen.dart +++ b/lib/screens/stream_screen.dart @@ -748,11 +748,22 @@ List _multiplyMatrices(List a, List b) { child: ColorFiltered( colorFilter: _buildColorFilter(), child: Mjpeg( + key: ValueKey(_streamUrl), stream: _streamUrl!, isLive: true, fit: BoxFit.contain, error: (context, error, stackTrace) { - return _buildErrorContent(); + // Mark as error so we can display error placeholder outside filters + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isError = true; + }); + } + }); + return SizedBox.expand( + child: Container(color: Colors.black), + ); }, ), ), @@ -845,21 +856,104 @@ List _multiplyMatrices(List a, List b) { } 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), + return Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error, color: _bulletColor, size: 100), + const SizedBox(height: 24), + Text( + _t("Unable to load camera stream"), + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _t("Please ensure:"), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon(Icons.wifi, color: Colors.white, size: 18), + const SizedBox(width: 12), + Expanded( + child: Text( + _t("WiFi is enabled"), + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon(Icons.signal_cellular_off, color: Colors.white, size: 18), + const SizedBox(width: 12), + Expanded( + child: Text( + _t("Mobile data is disabled"), + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton.icon( + onPressed: _loadStreamUrl, + icon: const Icon(Icons.refresh, size: 20), + label: Text(_t("Reload")), + style: ElevatedButton.styleFrom( + backgroundColor: _bulletColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + ), ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: _loadStreamUrl, - child: Text(_t("Retry")), - ), - ], + ), ); } } diff --git a/lib/screens/target_screen.dart b/lib/screens/target_screen.dart index 1e2452d..93b26a5 100644 --- a/lib/screens/target_screen.dart +++ b/lib/screens/target_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'stream_screen.dart'; // Import the camera stream screen +import 'spectator_screen.dart'; import '../utils/translations.dart'; class TargetScreen extends StatefulWidget { @@ -63,7 +64,6 @@ class _TargetScreenState extends State { }); } - @override Widget build(BuildContext context) { return Scaffold( @@ -72,59 +72,106 @@ class _TargetScreenState extends State { child: Column( children: [ Expanded( - child: GridView.builder( - itemCount: 6, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 1, // square-ish buttons - ), - itemBuilder: (context, index) { - final isEnabled = _cameraButtonsEnabled[index]; - return InkWell( - onTap: isEnabled - ? () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - CameraStreamScreen(cameraIndex: index + 1), - ), - ); - } - : null, - borderRadius: BorderRadius.circular(16), + child: Column( + children: [ + // Camera grid (6 cameras) + Expanded( + child: GridView.builder( + itemCount: 6, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1, + ), + itemBuilder: (context, index) { + final isEnabled = _cameraButtonsEnabled[index]; + return GestureDetector( + onTap: isEnabled + ? () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + CameraStreamScreen(cameraIndex: index + 1), + ), + ); + } + : null, + child: Container( + decoration: BoxDecoration( + color: isEnabled ? Colors.grey[900] : Colors.grey[300], + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.videocam_rounded, + color: isEnabled ? Colors.white : Colors.grey[500], + size: 36, + ), + const SizedBox(height: 12), + Text( + "${_t('Camera')} ${index + 1}", + textAlign: TextAlign.center, + style: TextStyle( + color: isEnabled ? Colors.white : Colors.grey[600], + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: 16), + // Spectate button (spans full width, half height) + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SpectatorScreen( + selectedLanguage: _selectedLanguage, + ), + ), + ); + }, child: Container( + height: 60, decoration: BoxDecoration( - color: isEnabled ? Colors.grey[900] : Colors.grey[300], + color: Colors.grey[900], borderRadius: BorderRadius.circular(16), ), padding: const EdgeInsets.all(16), - child: Column( + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.videocam_rounded, - color: isEnabled ? Colors.white : Colors.grey[500], - size: 36, + Icons.visibility_rounded, + color: Colors.white, + size: 28, ), - const SizedBox(height: 12), + const SizedBox(width: 12), Text( - "${_t('Camera')} ${index + 1}", + _t('Spectate'), textAlign: TextAlign.center, - style: TextStyle( - color: - isEnabled ? Colors.white : Colors.grey[600], - fontSize: 20, + style: const TextStyle( + color: Colors.white, + fontSize: 18, fontWeight: FontWeight.w600, ), ), ], ), ), - ); - }, + ), + ], ), ), ], diff --git a/lib/screens/target_view.dart b/lib/screens/target_view.dart index 212f588..a35b283 100644 --- a/lib/screens/target_view.dart +++ b/lib/screens/target_view.dart @@ -196,7 +196,14 @@ class _TargetViewScreenState extends State { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( - title: Text('📅 $_date', style: const TextStyle(color: Colors.white)), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.calendar_today, size: 20, color: Colors.white), + const SizedBox(width: 8), + Text(_date, style: const TextStyle(color: Colors.white)), + ], + ), backgroundColor: Colors.grey[900], centerTitle: true, iconTheme: const IconThemeData(color: Colors.white), @@ -377,27 +384,22 @@ class _TargetViewScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Combined badge with text and score - Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - color: Colors.grey[800], - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.grey[700]!, - width: 2, - ), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "🎯 ${_t('Total score')} $_totalScore", - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, + // Combined text with icon and score (no box) + FittedBox( + fit: BoxFit.scaleDown, + child: Row( + children: [ + const Icon(Icons.track_changes, size: 28, color: Colors.white), + const SizedBox(width: 8), + Text( + "${_t('Total score')} $_totalScore", + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), - ), + ], ), ), ], diff --git a/lib/utils/translations.dart b/lib/utils/translations.dart index 8ce3edc..1ed7351 100644 --- a/lib/utils/translations.dart +++ b/lib/utils/translations.dart @@ -584,6 +584,66 @@ class AppTranslations { 'English': 'copied to clipboard', 'Slovenian': 'kopirano v odložišče', }, + 'Select Cameras to Spectate': { + 'English': 'Select Cameras to Spectate', + 'Slovenian': 'Izberi kamere za ogled', + }, + 'Choose 1-6 cameras': { + 'English': 'Choose 1-6 cameras', + 'Slovenian': 'Izberi 1-6 kamer', + }, + 'No cameras configured': { + 'English': 'No cameras configured', + 'Slovenian': 'Ni nastavljenih kamer', + }, + 'View Cameras': { + 'English': 'View Cameras', + 'Slovenian': 'Prikaži kamere', + }, + 'Multi-Camera View': { + 'English': 'Multi-Camera View', + 'Slovenian': 'Več-kamerski pogled', + }, + 'Tap to return to grid': { + 'English': 'Tap to return to grid', + 'Slovenian': 'Tapni za vrnitev na mrežo', + }, + 'Stream unavailable': { + 'English': 'Stream unavailable', + 'Slovenian': 'Pretok ni na voljo', + }, + 'Select at least one camera': { + 'English': 'Select at least one camera', + 'Slovenian': 'Izberi vsaj eno kamero', + }, + 'Spectate': { + 'English': 'Spectate', + 'Slovenian': 'Opazuj', + }, + 'Select Cameras': { + 'English': 'Select Cameras', + 'Slovenian': 'Izberi kamere', + }, + 'Spectator Mode': { + 'English': 'Spectator Mode', + 'Slovenian': 'Način opazovanja', + }, + 'Please ensure:': { + 'English': 'Please ensure:', + 'Slovenian': 'Prepričaj se:', + }, + 'WiFi is enabled': { + 'English': 'WiFi is enabled', + 'Slovenian': 'WiFi je vključen', + }, + 'Mobile data is disabled': { + 'English': 'Mobile data is disabled', + 'Slovenian': 'Mobilni podatki so onemogočeni', + }, + 'Reload': { + 'English': 'Reload', + 'Slovenian': 'Ponovno naloži', + }, }; static String translate(String key, String language) {