emojis removed, updated headers, added spectate mode
This commit is contained in:
+138
-62
@@ -376,13 +376,24 @@ Future<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -473,15 +473,15 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
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<CalendarScreen> {
|
||||
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<CalendarScreen> {
|
||||
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<CalendarScreen> {
|
||||
),
|
||||
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(),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+259
-114
@@ -331,36 +331,169 @@ Future<void> _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<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() {
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -748,11 +748,22 @@ List<double> _multiplyMatrices(List<double> a, List<double> 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<double> _multiplyMatrices(List<double> a, List<double> 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")),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TargetScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -72,59 +72,106 @@ class _TargetScreenState extends State<TargetScreen> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -196,7 +196,14 @@ class _TargetViewScreenState extends State<TargetViewScreen> {
|
||||
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<TargetViewScreen> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user