emojis removed, updated headers, added spectate mode

This commit is contained in:
2025-12-20 10:17:23 +01:00
parent 5ae91d70e6
commit 3d033817a5
11 changed files with 1506 additions and 348 deletions
+138 -62
View File
@@ -376,13 +376,24 @@ Future<void> _renameFolder(String oldFolderName) async {
), ),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Center( child: Center(
child: Text( child: Row(
isTournament ? _t('Tournament Mode 🏆') : _t('Mark as Tournament 🏆'), mainAxisAlignment: MainAxisAlignment.center,
style: TextStyle( children: [
color: isTournament ? _bulletColor : Colors.white70, Icon(
fontWeight: FontWeight.w500, Icons.emoji_events,
fontSize: 16, 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'; : 'Unknown Date';
return Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey[300],
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
children: [ children: [
// Header section: Name and Date // Header section with grey background like toolbars
InkWell( InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -613,40 +624,56 @@ Widget _buildFolderRow(String folderName) {
topRight: Radius.circular(12), topRight: Radius.circular(12),
), ),
child: Container( 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
// Small colored indicator // White indicator line
Container( Container(
width: 4, width: 4,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _bulletColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
Text( Row(
_folderTypes[folderName] == true children: [
? '🏆 $folderName' if (_folderTypes[folderName] == true) ...[
: folderName, const Icon(Icons.emoji_events, size: 18, color: Colors.white),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _bulletColor), const SizedBox(width: 8),
],
Text(
folderName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
),
],
), ),
], ],
), ),
Row( Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.calendar_today, size: 14, color: Colors.white),
const SizedBox(width: 6),
Text( Text(
"📅 $formattedDate", formattedDate,
style: const TextStyle(fontSize: 13, color: Colors.black87), style: const TextStyle(fontSize: 13, color: Colors.white),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
Icon( Icon(
_expandedFolder == folderName ? Icons.expand_less : Icons.expand_more, _expandedFolder == folderName ? Icons.expand_less : Icons.expand_more,
color: Colors.black87, color: Colors.white,
), ),
], ],
), ),
@@ -698,7 +725,7 @@ Widget _buildFolderRow(String folderName) {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), const SizedBox(width: 4),
Text( Text(
"$totalScore", "$totalScore",
@@ -816,7 +843,7 @@ Widget _buildTargetsList(String folderName) {
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon( const Icon(
Icons.emoji_events, Icons.emoji_events,
color: Colors.amber, color: Colors.black87,
size: 14, size: 14,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@@ -893,13 +920,28 @@ Widget build(BuildContext context) {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Row(
_folderFilter == 'all' mainAxisSize: MainAxisSize.min,
? _t('Showing: 📂 All folders') children: [
: _folderFilter == 'tournament' Icon(
? _t('Showing: 🏆 Tournament folders') _folderFilter == 'all'
: _t('Showing: 🎯 Training folders'), ? Icons.folder
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black), : _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), Icon(Icons.filter_list, color: Colors.black87, size: 18),
], ],
@@ -917,6 +959,7 @@ Widget build(BuildContext context) {
thickness: 6, // optional: adjust width thickness: 6, // optional: adjust width
radius: const Radius.circular(8), radius: const Radius.circular(8),
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: _filteredFolderNames().length, itemCount: _filteredFolderNames().length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
String folderName = _filteredFolderNames()[index]; String folderName = _filteredFolderNames()[index];
@@ -955,11 +998,11 @@ void _showFolderFilterPopup(BuildContext context) {
const SizedBox(height: 24), const SizedBox(height: 24),
// Folder type filters // Folder type filters
_buildStyledFilterButton('all', _t('📂 All folders'), context), _buildStyledFilterButton('all', Icons.folder, _t('All folders'), context),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStyledFilterButton('tournament', _t('🏆 Tournament'), context), _buildStyledFilterButton('tournament', Icons.emoji_events, _t('Tournament'), context),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStyledFilterButton('training', _t('🎯 Training'), context), _buildStyledFilterButton('training', Icons.track_changes, _t('Training'), context),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -986,16 +1029,27 @@ void _showFolderFilterPopup(BuildContext context) {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: Center(
child: Text( child: Row(
_t('📅 Sort by Date'), mainAxisAlignment: MainAxisAlignment.center,
textAlign: TextAlign.center, children: [
style: TextStyle( Icon(
color: _sortMode == 'date' ? Colors.white : Colors.white70, Icons.calendar_today,
fontWeight: FontWeight.w500, size: 14,
fontSize: 14, color: _sortMode == 'date' ? Colors.white : Colors.white70,
), ),
maxLines: 2, const SizedBox(width: 4),
overflow: TextOverflow.ellipsis, 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), borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: Center(
child: Text( child: Row(
_t('🏆 Sort by Score'), mainAxisAlignment: MainAxisAlignment.center,
textAlign: TextAlign.center, children: [
style: TextStyle( Icon(
color: _sortMode == 'score' ? Colors.white : Colors.white70, Icons.emoji_events,
fontWeight: FontWeight.w500, size: 14,
fontSize: 14, color: _sortMode == 'score' ? Colors.white : Colors.white70,
), ),
maxLines: 2, const SizedBox(width: 4),
overflow: TextOverflow.ellipsis, 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; final isSelected = _folderFilter == value;
return SizedBox( return SizedBox(
@@ -1062,13 +1127,24 @@ Widget _buildStyledFilterButton(String value, String label, BuildContext context
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: Center(
child: Text( child: Row(
label, mainAxisAlignment: MainAxisAlignment.center,
style: TextStyle( children: [
color: isSelected ? Colors.white : Colors.white70, Icon(
fontSize: 16, icon,
fontWeight: FontWeight.w500, 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,
),
),
],
), ),
), ),
), ),
+23 -25
View File
@@ -473,15 +473,15 @@ class _CalendarScreenState extends State<CalendarScreen> {
onTap: () => _makePhoneCall(contact.phone), onTap: () => _makePhoneCall(contact.phone),
onLongPress: () => _showContactPopup(context, contact), onLongPress: () => _showContactPopup(context, contact),
child: Container( child: Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey[300],
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withValues(alpha: 0.08), color: Colors.grey.withValues(alpha: 0.3),
blurRadius: 2, blurRadius: 8,
offset: const Offset(0, 1), offset: const Offset(0, 2),
), ),
], ],
), ),
@@ -490,37 +490,37 @@ class _CalendarScreenState extends State<CalendarScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Colored header bar (like event cards) // Grey header bar with white text (matching book screen style)
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.2), color: Colors.grey[900],
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10), topLeft: Radius.circular(12),
topRight: Radius.circular(10), topRight: Radius.circular(12),
), ),
), ),
child: Row( child: Row(
children: [ children: [
// Small colored indicator // White indicator line
Container( Container(
width: 3, width: 4,
height: 12, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: accentColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 7), const SizedBox(width: 12),
// Contact name // Contact name in white
Expanded( Expanded(
child: Text( child: Text(
_capitalize(contact.fullName), _capitalize(contact.fullName),
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: accentColor, color: Colors.white,
), ),
), ),
), ),
@@ -1154,9 +1154,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isToday color: cardAccentColor,
? cardAccentColor.withValues(alpha: 0.2)
: cardAccentColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
topRight: Radius.circular(12), topRight: Radius.circular(12),
@@ -1164,22 +1162,22 @@ class _CalendarScreenState extends State<CalendarScreen> {
), ),
child: Row( child: Row(
children: [ children: [
// Small colored indicator // Small white indicator line
Container( Container(
width: 4, width: 4,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: cardAccentColor, color: Colors.white,
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
dateText, dateText,
style: TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: cardAccentColor, color: Colors.white,
), ),
), ),
const Spacer(), const Spacer(),
+137
View File
@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import '../utils/translations.dart';
class CameraSelectionDialog extends StatefulWidget {
final List<Map<String, dynamic>> availableCameras;
final String selectedLanguage;
const CameraSelectionDialog({
super.key,
required this.availableCameras,
required this.selectedLanguage,
});
@override
State<CameraSelectionDialog> createState() => _CameraSelectionDialogState();
}
class _CameraSelectionDialogState extends State<CameraSelectionDialog> {
late List<bool> _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 = <Map<String, dynamic>>[];
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,
),
),
),
],
);
}
}
+248
View File
@@ -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<Map<String, dynamic>> cameras;
final String selectedLanguage;
const MultiCameraScreen({
super.key,
required this.cameras,
required this.selectedLanguage,
});
@override
State<MultiCameraScreen> createState() => _MultiCameraScreenState();
}
class _MultiCameraScreenState extends State<MultiCameraScreen> {
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,
),
),
],
),
);
}
}
+83 -74
View File
@@ -576,9 +576,17 @@ void _moveBullet(Offset offsetDelta) {
), ),
child: Column( child: Column(
children: [ children: [
Text( Row(
'🎯 $nextTraining', mainAxisAlignment: MainAxisAlignment.center,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 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), const SizedBox(height: 4),
Text( Text(
@@ -611,9 +619,17 @@ void _moveBullet(Offset offsetDelta) {
), ),
child: Column( child: Column(
children: [ children: [
Text( Row(
'🏆 $nextTournament', mainAxisAlignment: MainAxisAlignment.center,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 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), const SizedBox(height: 4),
Text( Text(
@@ -656,9 +672,21 @@ void _moveBullet(Offset offsetDelta) {
), ),
child: Column( child: Column(
children: [ children: [
Text( Row(
'${lastIsTournament ? "🏆" : "🎯"} $lastFolder', mainAxisAlignment: MainAxisAlignment.center,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 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), const SizedBox(height: 4),
Text( Text(
@@ -982,48 +1010,34 @@ Widget build(BuildContext context) {
), ),
], ],
), ),
child: Column( child: Container(
children: [ decoration: BoxDecoration(
Container( color: _getBulletButtonColor(index),
height: 30, borderRadius: BorderRadius.circular(16),
decoration: BoxDecoration( ),
color: Colors.grey[300], alignment: Alignment.center,
borderRadius: const BorderRadius.only( child: Column(
topLeft: Radius.circular(16), mainAxisAlignment: MainAxisAlignment.center,
topRight: Radius.circular(16), children: [
), Text(
),
alignment: Alignment.center,
child: Text(
'${index + 1}', '${index + 1}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 12,
color: Colors.black87, color: Colors.white,
), ),
), ),
), const SizedBox(height: 4),
Expanded( Text(
child: Container( '$score',
decoration: BoxDecoration( style: const TextStyle(
color: _getBulletButtonColor(index), fontSize: 28,
borderRadius: const BorderRadius.only( fontWeight: FontWeight.bold,
bottomLeft: Radius.circular(16), color: Colors.white,
bottomRight: Radius.circular(16),
),
),
alignment: Alignment.center,
child: Text(
'$score',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
), ),
), ),
), ],
], ),
), ),
), ),
), ),
@@ -1095,41 +1109,36 @@ Widget build(BuildContext context) {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Left side - Label (darker grey) // Left side - Label with icon
const Padding( Padding(
padding: EdgeInsets.only(right: 16), padding: const EdgeInsets.only(right: 16),
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Row(
"🎯 Total Score:", children: [
style: TextStyle( const Icon(Icons.track_changes, size: 24, color: Colors.white),
fontSize: 28, const SizedBox(width: 8),
fontWeight: FontWeight.bold, const Text(
color: Colors.white, "Total Score:",
), style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
), ),
), ),
), ),
// Right side - Score badge (grey with rounded corners) // Right side - Score number (no box)
Container( FittedBox(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), fit: BoxFit.scaleDown,
decoration: BoxDecoration( child: Text(
color: Colors.grey[800], "$_totalScore",
borderRadius: BorderRadius.circular(16), style: const TextStyle(
border: Border.all( fontSize: 28,
color: Colors.grey[700]!, fontWeight: FontWeight.bold,
width: 2, color: Colors.white,
),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"$_totalScore",
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
), ),
), ),
), ),
+259 -114
View File
@@ -331,36 +331,169 @@ Future<void> _saveLanguage(String language) async {
orElse: () => _languages[0], orElse: () => _languages[0],
); );
return Column( return Container(
crossAxisAlignment: CrossAxisAlignment.start, margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
children: [ decoration: BoxDecoration(
Text( color: Colors.grey[300],
_t("Language"), borderRadius: BorderRadius.circular(12),
style: const TextStyle( boxShadow: [
fontSize: 16, BoxShadow(
fontWeight: FontWeight.bold, color: Colors.grey.withValues(alpha: 0.3),
color: Colors.black87, blurRadius: 8,
offset: const Offset(0, 2),
), ),
), ],
const SizedBox(height: 8), ),
GestureDetector( child: Column(
onTap: _showLanguagePopup, crossAxisAlignment: CrossAxisAlignment.start,
child: Container( children: [
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), // Header
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey[900],
borderRadius: BorderRadius.circular(12), borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
), ),
child: Row( child: Row(
children: [ children: [
Text( Container(
selectedLang['flag'] ?? '🌐', width: 4,
style: const TextStyle(fontSize: 20), height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2),
),
), ),
const SizedBox(width: 12), 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( Expanded(
child: Text( child: Text(
_t(selectedLang['name'] ?? 'English'), _t(_selectedBulletColor),
style: const TextStyle(color: Colors.black87, fontSize: 14), style: const TextStyle(color: Colors.black87, fontSize: 14),
), ),
), ),
@@ -370,54 +503,7 @@ Future<void> _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() { Widget _buildCameraList() {
return Column( return Container(
crossAxisAlignment: CrossAxisAlignment.start, margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
children: [ decoration: BoxDecoration(
Text( color: Colors.grey[300],
_t("Camera Settings"), borderRadius: BorderRadius.circular(12),
style: const TextStyle( boxShadow: [
fontSize: 16, BoxShadow(
fontWeight: FontWeight.bold, color: Colors.grey.withValues(alpha: 0.3),
color: Colors.black87, blurRadius: 8,
offset: const Offset(0, 2),
), ),
), ],
const SizedBox(height: 8), ),
...List.generate(6, (index) { child: Column(
return GestureDetector( crossAxisAlignment: CrossAxisAlignment.start,
onTap: () => _editCameraIP(index), children: [
child: Container( // Header
margin: const EdgeInsets.symmetric(vertical: 4), Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), width: double.infinity,
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: Colors.grey[300], decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), color: Colors.grey[900],
), borderRadius: const BorderRadius.only(
child: Row( topLeft: Radius.circular(12),
mainAxisAlignment: MainAxisAlignment.spaceBetween, topRight: Radius.circular(12),
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: 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),
),
),
),
],
),
);
}),
],
),
); );
} }
+342
View File
@@ -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<SpectatorScreen> createState() => _SpectatorScreenState();
}
class _SpectatorScreenState extends State<SpectatorScreen> {
List<bool> _selectedCameras = List.generate(6, (_) => true); // All selected by default
List<bool> _enabledCameras = List.generate(6, (_) => false); // Which cameras have IPs configured
List<Map<String, dynamic>> _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<void> _loadCameras() async {
final prefs = await SharedPreferences.getInstance();
List<Map<String, dynamic>> cameras = [];
List<bool> 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<void> _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 = <Map<String, dynamic>>[];
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,
),
),
),
),
),
],
),
),
),
);
},
),
);
}
}
+109 -15
View File
@@ -748,11 +748,22 @@ List<double> _multiplyMatrices(List<double> a, List<double> b) {
child: ColorFiltered( child: ColorFiltered(
colorFilter: _buildColorFilter(), colorFilter: _buildColorFilter(),
child: Mjpeg( child: Mjpeg(
key: ValueKey(_streamUrl),
stream: _streamUrl!, stream: _streamUrl!,
isLive: true, isLive: true,
fit: BoxFit.contain, fit: BoxFit.contain,
error: (context, error, stackTrace) { 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<double> _multiplyMatrices(List<double> a, List<double> b) {
} }
Widget _buildErrorContent() { Widget _buildErrorContent() {
return Column( return Center(
mainAxisAlignment: MainAxisAlignment.center, child: SingleChildScrollView(
children: [ child: Padding(
const Icon(Icons.error, color: Colors.red, size: 100), padding: const EdgeInsets.all(24),
const SizedBox(height: 20), child: Container(
Text( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
_t("Unable to load camera stream"), decoration: BoxDecoration(
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), 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")),
),
],
); );
} }
} }
+84 -37
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'stream_screen.dart'; // Import the camera stream screen import 'stream_screen.dart'; // Import the camera stream screen
import 'spectator_screen.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
class TargetScreen extends StatefulWidget { class TargetScreen extends StatefulWidget {
@@ -63,7 +64,6 @@ class _TargetScreenState extends State<TargetScreen> {
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -72,59 +72,106 @@ class _TargetScreenState extends State<TargetScreen> {
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
child: GridView.builder( child: Column(
itemCount: 6, children: [
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( // Camera grid (6 cameras)
crossAxisCount: 2, Expanded(
mainAxisSpacing: 16, child: GridView.builder(
crossAxisSpacing: 16, itemCount: 6,
childAspectRatio: 1, // square-ish buttons gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
), crossAxisCount: 2,
itemBuilder: (context, index) { mainAxisSpacing: 16,
final isEnabled = _cameraButtonsEnabled[index]; crossAxisSpacing: 16,
return InkWell( childAspectRatio: 1,
onTap: isEnabled ),
? () { itemBuilder: (context, index) {
Navigator.push( final isEnabled = _cameraButtonsEnabled[index];
context, return GestureDetector(
MaterialPageRoute( onTap: isEnabled
builder: (context) => ? () {
CameraStreamScreen(cameraIndex: index + 1), Navigator.push(
), context,
); MaterialPageRoute(
} builder: (context) =>
: null, CameraStreamScreen(cameraIndex: index + 1),
borderRadius: BorderRadius.circular(16), ),
);
}
: 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( child: Container(
height: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isEnabled ? Colors.grey[900] : Colors.grey[300], color: Colors.grey[900],
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.videocam_rounded, Icons.visibility_rounded,
color: isEnabled ? Colors.white : Colors.grey[500], color: Colors.white,
size: 36, size: 28,
), ),
const SizedBox(height: 12), const SizedBox(width: 12),
Text( Text(
"${_t('Camera')} ${index + 1}", _t('Spectate'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
color: color: Colors.white,
isEnabled ? Colors.white : Colors.grey[600], fontSize: 18,
fontSize: 20,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
], ],
), ),
), ),
); ),
}, ],
), ),
), ),
], ],
+23 -21
View File
@@ -196,7 +196,14 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( 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], backgroundColor: Colors.grey[900],
centerTitle: true, centerTitle: true,
iconTheme: const IconThemeData(color: Colors.white), iconTheme: const IconThemeData(color: Colors.white),
@@ -377,27 +384,22 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Combined badge with text and score // Combined text with icon and score (no box)
Container( FittedBox(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), fit: BoxFit.scaleDown,
decoration: BoxDecoration( child: Row(
color: Colors.grey[800], children: [
borderRadius: BorderRadius.circular(16), const Icon(Icons.track_changes, size: 28, color: Colors.white),
border: Border.all( const SizedBox(width: 8),
color: Colors.grey[700]!, Text(
width: 2, "${_t('Total score')} $_totalScore",
), style: const TextStyle(
), fontSize: 28,
child: FittedBox( fontWeight: FontWeight.bold,
fit: BoxFit.scaleDown, color: Colors.white,
child: Text( ),
"🎯 ${_t('Total score')} $_totalScore",
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ],
), ),
), ),
], ],
+60
View File
@@ -584,6 +584,66 @@ class AppTranslations {
'English': 'copied to clipboard', 'English': 'copied to clipboard',
'Slovenian': 'kopirano v odložišče', '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) { static String translate(String key, String language) {