diff --git a/assets/targets/Target19.jpg b/assets/targets/Target19.jpg index 66471aa..bc0840d 100644 Binary files a/assets/targets/Target19.jpg and b/assets/targets/Target19.jpg differ diff --git a/assets/targets/Target20.jpg b/assets/targets/Target20.jpg index b17a5d8..dd2b8e3 100644 Binary files a/assets/targets/Target20.jpg and b/assets/targets/Target20.jpg differ diff --git a/assets/targets/Target22.jpg b/assets/targets/Target22.jpg index 20a528b..89ae2ba 100644 Binary files a/assets/targets/Target22.jpg and b/assets/targets/Target22.jpg differ diff --git a/assets/targets/Target23.jpg b/assets/targets/Target23.jpg index f54669e..ffedb53 100644 Binary files a/assets/targets/Target23.jpg and b/assets/targets/Target23.jpg differ diff --git a/assets/targets/Target24.jpg b/assets/targets/Target24.jpg index d36f3dc..2e319d3 100644 Binary files a/assets/targets/Target24.jpg and b/assets/targets/Target24.jpg differ diff --git a/assets/targets/Target25.jpg b/assets/targets/Target25.jpg new file mode 100644 index 0000000..7b98032 Binary files /dev/null and b/assets/targets/Target25.jpg differ diff --git a/assets/targets/Target26.jpg b/assets/targets/Target26.jpg new file mode 100644 index 0000000..a7f186e Binary files /dev/null and b/assets/targets/Target26.jpg differ diff --git a/assets/targets/Target27.jpg b/assets/targets/Target27.jpg new file mode 100644 index 0000000..daa81bf Binary files /dev/null and b/assets/targets/Target27.jpg differ diff --git a/assets/targets/Target28.jpg b/assets/targets/Target28.jpg new file mode 100644 index 0000000..47dd120 Binary files /dev/null and b/assets/targets/Target28.jpg differ diff --git a/assets/targets/Target29.jpg b/assets/targets/Target29.jpg new file mode 100644 index 0000000..ffe221c Binary files /dev/null and b/assets/targets/Target29.jpg differ diff --git a/assets/targets/Target30.jpg b/assets/targets/Target30.jpg new file mode 100644 index 0000000..e806fe8 Binary files /dev/null and b/assets/targets/Target30.jpg differ diff --git a/lib/screens/book_screen.dart b/lib/screens/book_screen.dart index d48f69c..4a818e8 100644 --- a/lib/screens/book_screen.dart +++ b/lib/screens/book_screen.dart @@ -227,7 +227,7 @@ Color _getColorFromName(String colorName) { await file.delete(); _loadTargetDataFiles(); // Refresh after deleting the file } catch (e) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Error deleting file: $e"))); + debugPrint("Error deleting file: $e"); } } } @@ -380,7 +380,7 @@ Future _renameFolder(String oldFolderName) async { child: Container( width: double.infinity, decoration: BoxDecoration( - color: isTournament ? _bulletColor.withOpacity(0.2) : Colors.grey[850], + color: isTournament ? _bulletColor.withValues(alpha: 0.2) : Colors.grey[850], borderRadius: BorderRadius.circular(12), border: Border.all( color: isTournament ? _bulletColor : Colors.grey[700]!, @@ -469,9 +469,7 @@ Future _renameFolder(String oldFolderName) async { if (targetFolderName != oldFolderName) { if (await newFolderPath.exists()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('A folder named "$targetFolderName" already exists.')), - ); + debugPrint('A folder named "$targetFolderName" already exists.'); return; } else { await oldFolderPath.rename(newFolderPath.path); @@ -522,16 +520,8 @@ Future _renameFolder(String oldFolderName) async { } _loadTargetDataFiles(); // Refresh file/folder list - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Folder "$folderName" deleted successfully.'), - ), - ); } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error deleting folder: $e")), - ); + debugPrint("Error deleting folder: $e"); } } } diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index ce2bdd1..f09af2c 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -957,7 +957,7 @@ class _CalendarScreenState extends State { time = '18:00 - 19:00'; break; case EventType.regular: - cardAccentColor = Colors.blue; + cardAccentColor = accentColor; eventIcon = Icons.event; title = _t('Duty Shift'); time = '14:00 - 16:00'; @@ -1112,7 +1112,7 @@ class _CalendarScreenState extends State { time = '18:00 - 19:00'; break; case EventType.regular: - cardAccentColor = Colors.blue; + cardAccentColor = accentColor; eventIcon = Icons.event; title = _t('Duty Shift'); time = '14:00 - 16:00'; @@ -1253,7 +1253,7 @@ class _CalendarScreenState extends State { eventIcon = Icons.groups; // People icon for meetings break; case EventType.regular: - cardAccentColor = Colors.blue; + cardAccentColor = accentColor; backgroundColor = Colors.grey[300]!; eventIcon = Icons.event; break; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index d990926..a7b828c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -10,7 +10,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../utils/translations.dart'; import '../utils/target_colors.dart'; - class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -62,7 +61,8 @@ class _HomeScreenState extends State { Future _loadSelectedTarget() async { final prefs = await SharedPreferences.getInstance(); - final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png'; + final selectedTarget = + prefs.getString('selectedTarget') ?? 'assets/target.png'; setState(() { _selectedTarget = selectedTarget; _backgroundColor = TargetColors.getBackgroundColor(selectedTarget); @@ -139,4 +139,3 @@ class _HomeScreenState extends State { ); } } - diff --git a/lib/screens/preview_screen.dart b/lib/screens/preview_screen.dart index 0528c24..ffbc257 100644 --- a/lib/screens/preview_screen.dart +++ b/lib/screens/preview_screen.dart @@ -5,17 +5,104 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; import 'yolo_detector.dart'; -import 'dart:convert'; // For JSON encoding +import 'dart:convert'; // For JSON encoding import 'dart:math'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/translations.dart'; +// DEBUG: Set to true to show scoring rings visualization +const bool _DEBUG_SHOW_SCORING_RINGS = false; + +class ScoringRingsPainter extends CustomPainter { + final Offset center; + final double scaleX; + final double scaleY; + final double targetScaleFactorX; + final double targetScaleFactorY; + + ScoringRingsPainter({ + required this.center, + required this.scaleX, + required this.scaleY, + required this.targetScaleFactorX, + required this.targetScaleFactorY, + }); + + @override + void paint(Canvas canvas, Size size) { + // Colors for each ring (10 to 1) + final List ringColors = [ + Colors.red, // 10 + Colors.orange, // 9 + Colors.yellow, // 8 + Colors.lime, // 7 + Colors.green, // 6 + Colors.cyan, // 5 + Colors.blue, // 4 + Colors.indigo, // 3 + Colors.purple, // 2 + Colors.pink, // 1 + ]; + + // Scoring rings distances from center (in default 1200x1200 space) + // Must match the rings in _generatePositionForScore + final List rings = [14, 68, 123, 177, 232, 286, 342, 397, 455, 510]; + + // The canvas is already positioned at the image location + // Center should be scaled by target scale factors first (default 1200x1200 to actual target size) + // then by display scale factors (target image to screen) + final scaledCenter = Offset( + center.dx * targetScaleFactorX, + center.dy * targetScaleFactorY, + ); + final displayCenter = Offset( + scaledCenter.dx * scaleX, + scaledCenter.dy * scaleY, + ); + + // Draw each scoring ring with a different color + for (int i = 0; i < rings.length; i++) { + final paint = Paint() + ..color = ringColors[i].withValues(alpha: 0.5) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + final ringRadius = rings[i]; + // Scale by target scale factors (default 1200x1200 to actual target size) + // then by display scale factors (target image to screen) + final scaledRadiusX = ringRadius * targetScaleFactorX * scaleX; + final scaledRadiusY = ringRadius * targetScaleFactorY * scaleY; + final scaledRadius = (scaledRadiusX + scaledRadiusY) / 2; + canvas.drawCircle(displayCenter, scaledRadius, paint); + } + + // Draw center point + final centerPaint = Paint() + ..color = Colors.red + ..style = PaintingStyle.fill; + canvas.drawCircle(displayCenter, 4, centerPaint); + } + + @override + bool shouldRepaint(ScoringRingsPainter oldDelegate) { + return center != oldDelegate.center || + scaleX != oldDelegate.scaleX || + scaleY != oldDelegate.scaleY || + targetScaleFactorX != oldDelegate.targetScaleFactorX || + targetScaleFactorY != oldDelegate.targetScaleFactorY; + } +} + class PreviewScreen extends StatefulWidget { final String imagePath; final List detections; final int bulletCount; // Add bulletCount parameter - const PreviewScreen({super.key, required this.imagePath, required this.detections, required this.bulletCount}); + const PreviewScreen( + {super.key, + required this.imagePath, + required this.detections, + required this.bulletCount}); @override State createState() => _PreviewScreenState(); @@ -25,7 +112,7 @@ enum PreviewMode { cameraWithDetections, mappedTarget } class _PreviewScreenState extends State { bool _isLoading = true; - PreviewMode _mode = PreviewMode.cameraWithDetections; + PreviewMode _mode = PreviewMode.mappedTarget; List _mappedBullets = []; late List _realImageDetections; @@ -40,9 +127,16 @@ class _PreviewScreenState extends State { late String targetAssetPath; - final Offset fixedRingTargetCenter = const Offset(602.51, 600.43); + final Offset fixedRingTargetCenter = const Offset(601, 598); final Size fixedRingTargetSize = const Size(355.94, 359.27); + // Scale factors for different target image dimensions + double _scaleFactorX = 1.0; + double _scaleFactorY = 1.0; + img.Image? _defaultTargetDecoded; // Default target for reference dimensions + Offset? + _scaledTargetCenterForScoring; // Scaled center for scoring with display target + int? _highlightedBulletIndex; final List _manualBulletScores = []; @@ -55,7 +149,7 @@ class _PreviewScreenState extends State { Color get _editColor => Colors.deepOrangeAccent; Color get _activeColor => _isEditMode ? _editColor : _bulletColor; - Color get _activeColorWithOpacity => _activeColor.withOpacity(_bulletOpacity); + Color get _activeColorWithOpacity => _activeColor.withValues(alpha: _bulletOpacity); String _selectedLanguage = 'English'; Color _backgroundColor = Colors.grey[900]!; @@ -70,6 +164,7 @@ class _PreviewScreenState extends State { Future _initializeScreen() async { await Future.wait([ + _loadDefaultTarget(), // Load default target for scale reference _loadSelectedTarget(), _loadBulletColor(), _loadLanguage(), @@ -88,13 +183,22 @@ class _PreviewScreenState extends State { }); } + Future _loadDefaultTarget() async { + try { + final byteData = await rootBundle.load('assets/target.png'); + final bytes = byteData.buffer.asUint8List(); + _defaultTargetDecoded = img.decodeImage(bytes); + } catch (e) { + debugPrint("❌ Error loading default target: $e"); + } + } + Future _loadSelectedTarget() async { final prefs = await SharedPreferences.getInstance(); - // For detection mode (cameraWithDetections), always use default target - // For presentation mode (mappedTarget), use selected target - final selectedTarget = _mode == PreviewMode.mappedTarget - ? (prefs.getString('selectedTarget') ?? 'assets/target.png') - : 'assets/target.png'; + // Always load the selected/skin target for display + // This is used for background and for calculating scale factors + final selectedTarget = + prefs.getString('selectedTarget') ?? 'assets/target.png'; // Update the target path and trigger image load targetAssetPath = selectedTarget; @@ -104,7 +208,10 @@ class _PreviewScreenState extends State { // Await the image decoding to ensure it's populated before rebuilding _targetImageDecoded = await _targetImageFuture; if (_targetImageDecoded != null) { - _cachedTargetImage = Uint8List.fromList(img.encodePng(_targetImageDecoded!)); + _cachedTargetImage = + Uint8List.fromList(img.encodePng(_targetImageDecoded!)); + // Calculate scale factors based on target dimensions + _calculateScaleFactors(); } setState(() { @@ -112,6 +219,27 @@ class _PreviewScreenState extends State { }); } + void _calculateScaleFactors() { + if (_defaultTargetDecoded == null || _targetImageDecoded == null) { + _scaleFactorX = 1.0; + _scaleFactorY = 1.0; + _scaledTargetCenterForScoring = null; + return; + } + _scaleFactorX = _targetImageDecoded!.width / _defaultTargetDecoded!.width; + _scaleFactorY = _targetImageDecoded!.height / _defaultTargetDecoded!.height; + + // Scale the center point for scoring + if (_targetImageCenterForScoring != null) { + _scaledTargetCenterForScoring = Offset( + _targetImageCenterForScoring!.dx * _scaleFactorX, + _targetImageCenterForScoring!.dy * _scaleFactorY, + ); + } + debugPrint( + "πŸ“ Scale factors calculated: X=$_scaleFactorX, Y=$_scaleFactorY"); + } + Color _getBackgroundColorForTarget(String targetPath) { // Map each target to its background color (extracted from first pixel of each image) switch (targetPath) { @@ -167,28 +295,46 @@ class _PreviewScreenState extends State { return const Color(0xFF000000); // Black case 'assets/targets/Target17.jpg': - return const Color(0xFF13132B); // Dark blue + return const Color(0xFF21304D); // Dark blue case 'assets/targets/Target18.jpg': return const Color(0xFF050608); // Very dark gray case 'assets/targets/Target19.jpg': - return const Color(0xFFC32534); // Dark red + return const Color(0xFF09080D); // Dark red case 'assets/targets/Target20.jpg': - return const Color(0xFFFCF3EE); // Light beige + return const Color(0xFFA9D9EF); // Light blue case 'assets/targets/Target21.jpg': return const Color(0xFFDA6868); // Muted red case 'assets/targets/Target22.jpg': - return const Color(0xFFFDFAF3); // Off-white + return const Color(0xFFFBFAF6); // Off-white case 'assets/targets/Target23.jpg': - return const Color(0xFFFFFFFD); // White + return const Color(0xFF000000); // Black case 'assets/targets/Target24.jpg': - return const Color(0xFFFFC4C6); // Light pink + return const Color(0xFF000000); // Black + + case 'assets/targets/Target25.jpg': + return const Color(0xFF040308); // Very dark gray + + case 'assets/targets/Target26.jpg': + return const Color(0xFF161C2C); // Dark blue-gray + + case 'assets/targets/Target27.jpg': + return const Color(0xFF28A5DF); // Blue + + case 'assets/targets/Target28.jpg': + return const Color(0xFFFFFFFF); // White + + case 'assets/targets/Target29.jpg': + return const Color(0xFF180E27); // Dark purple + + case 'assets/targets/Target30.jpg': + return const Color(0xFFFDFDFB); // Off-white default: return Colors.grey[900]!; // Fallback to dark gray @@ -196,69 +342,88 @@ class _PreviewScreenState extends State { } Future _loadBulletColor() async { - final prefs = await SharedPreferences.getInstance(); - final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; + final prefs = await SharedPreferences.getInstance(); + final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; - setState(() { - _bulletColor = _getColorFromName(selectedColor); - }); -} - -Color _getColorFromName(String colorName) { - switch (colorName.toLowerCase()) { - case 'blue': - return Colors.blue; - case 'green': - return Colors.green; - case 'yellow': - return Colors.yellow; - case 'purple': - return Colors.purple; - case 'cyan': - return Colors.cyan; - case 'teal': - return Colors.teal; - case 'indigo': - return Colors.indigo; - case 'pink': - return Colors.pink; - case 'lime': - return Colors.lime; - case 'mint': - return Colors.greenAccent; - case 'orange': - return Colors.orange; - case 'violet': - return Colors.deepPurpleAccent; - case 'amber': - return Colors.amber; - case 'brown': - return Colors.brown; - case 'grey': - return Colors.grey; - default: - return Colors.blue; // fallback + setState(() { + _bulletColor = _getColorFromName(selectedColor); + }); } -} + Color _getColorFromName(String colorName) { + switch (colorName.toLowerCase()) { + case 'blue': + return Colors.blue; + case 'green': + return Colors.green; + case 'yellow': + return Colors.yellow; + case 'purple': + return Colors.purple; + case 'cyan': + return Colors.cyan; + case 'teal': + return Colors.teal; + case 'indigo': + return Colors.indigo; + case 'pink': + return Colors.pink; + case 'lime': + return Colors.lime; + case 'mint': + return Colors.greenAccent; + case 'orange': + return Colors.orange; + case 'violet': + return Colors.deepPurpleAccent; + case 'amber': + return Colors.amber; + case 'brown': + return Colors.brown; + case 'grey': + return Colors.grey; + default: + return Colors.blue; // fallback + } + } - Offset _generatePositionForScore(int score, Offset center) { - const double bulletRadius = 25; - final List rings = [14, 70, 125, 180, 235, 290, 345, 400, 455, 510]; + Offset _generatePositionForScore(int score) { + const double bulletRadius = 25; + final List rings = [14, 68, 123, 177, 232, 286, 342, 397, 455, 510]; - final rng = score.clamp(0, 10); - final double distance = rng == 0 ? rings.last + bulletRadius + 5 : rings[10 - rng]; + // Always generate positions in the default 1200x1200 coordinate system + final center = fixedRingTargetCenter; - final angle = (360 / widget.bulletCount) * (_manualBulletScores.length + 1); - final radians = angle * 3.14159 / 180; + final rng = score.clamp(0, 10); + final double distance; - return Offset( - center.dx + distance * cos(radians), - center.dy + distance * sin(radians), - ); -} + if (rng == 0) { + // Score 0: place outside all rings (bullet center beyond the outermost ring) + distance = rings.last + bulletRadius; + } else if (rng == 10) { + // Score 10: place in the center ring (0-14) + distance = rings[0] / 2; + } else { + // Scores 1-9: place in the middle of the ring + final outerRadius = rings[10 - rng]; + final innerRadius = rng < 10 ? rings[10 - rng - 1] : 0; + distance = (outerRadius + innerRadius) / 2; + } + // Calculate angle based on which manual bullet this will be (add randomness for distribution) + final bulletIndex = _manualBulletScores.length; + final angleIncrement = 360.0 / widget.bulletCount; + final baseAngle = angleIncrement * bulletIndex; + // Add random offset within the angle increment to vary positioning + final randomOffset = (Random().nextDouble() - 0.5) * angleIncrement * 0.5; + final angle = baseAngle + randomOffset; + final radians = angle * 3.14159 / 180; + return Offset( + center.dx + distance * cos(radians), + center.dy + distance * sin(radians), + ); + } Future _loadAndDecodeTargetImage() async { final byteData = await rootBundle.load(targetAssetPath); @@ -268,559 +433,784 @@ Color _getColorFromName(String colorName) { await targetFile.writeAsBytes(bytes); final decoded = img.decodeImage(bytes); if (decoded != null) { - _targetImageCenterForScoring = Offset(decoded.width / 2, decoded.height / 2 - 1); + _targetImageCenterForScoring = + Offset(decoded.width / 2, decoded.height / 2 - 1); } return decoded; } Future _runDetectionAndMapping() async { - try { - // 1. Read the real image file - final File realImageFile = File(widget.imagePath); - final realBytes = await realImageFile.readAsBytes(); - - // 2. Decode the real image (ensure it's valid) - _realImageDecoded = img.decodeImage(realBytes); - if (_realImageDecoded == null) { - debugPrint("❌ Failed to decode image from path: ${widget.imagePath}"); - setState(() => _isLoading = false); - return; - } - - // 3. Cache decoded image as PNG for rendering - _cachedRealImage = Uint8List.fromList(img.encodePng(_realImageDecoded!)); - - // 4. Find the detected ring center and size - final ringCamData = _getRingCenterSize(_realImageDetections); - if (ringCamData == null) { - debugPrint("⚠️ Ring (classId 2) not detected in real image."); - setState(() => _isLoading = false); - return; - } - - final Offset ringCamCenter = ringCamData['center']; - final Size sizeCam = ringCamData['size']; - - // 5. Extract all bullet centers (classId 0) - final List bulletsCam = _realImageDetections - .where((r) => r.classId == 0) - .map((r) => Offset((r.x1 + r.x2) / 2, (r.y1 + r.y2) / 2)) - .toList(); - - // 6. Map bullets to target and score each one - final List> scoredBullets = bulletsCam.map((bullet) { - final mappedBullet = _mapToTargetNormalized( - bullet, - ringCamCenter, - fixedRingTargetCenter, - sizeCam, - fixedRingTargetSize, - ); - final score = _scoreForBullet( - mappedBullet, - _targetImageCenterForScoring ?? fixedRingTargetCenter, - ); - return {'bullet': mappedBullet, 'score': score}; - }).toList(); - - // 7. Trim to bullet limit by removing highest scoring ones - scoredBullets.sort((a, b) => b['score'].compareTo(a['score'])); - while (scoredBullets.length > widget.bulletCount) { - scoredBullets.removeAt(0); - } - - // 8. Collect final mapped bullets - final List mappedBullets = scoredBullets - .map((e) => e['bullet'] as Offset) - .toList(); - - // 9. Load target background image - _targetImageDecoded = await _targetImageFuture; - if (_targetImageDecoded != null) { - _cachedTargetImage = Uint8List.fromList(img.encodePng(_targetImageDecoded!)); - } - - // 10. Update state with mapped bullets - setState(() { - _mappedBullets = mappedBullets; - _isLoading = false; - }); - } catch (e, stack) { - debugPrint("❌ Error during detection mapping: $e"); - debugPrint(stack.toString()); - setState(() { - _mappedBullets = []; - _isLoading = false; - }); - } -} - -void _manualScoreEntry() { - final missingCount = widget.bulletCount - _mappedBullets.length; - final controllers = List.generate(missingCount, (_) => TextEditingController()); - - showDialog( - context: context, - builder: (context) { - return Dialog( - backgroundColor: Colors.grey[900], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _t('Enter Missing Bullet Scores'), - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: SingleChildScrollView( - child: Column( - children: List.generate(missingCount, (index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: TextField( - controller: controllers[index], - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - labelText: '${_t('Bullet Score')} ${_mappedBullets.length + index + 1}', - labelStyle: const TextStyle(color: Colors.white54), - filled: true, - fillColor: Colors.grey[800], - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: _bulletColor), - ), - ), - ), - ); - }), - ), - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - final scores = controllers.map((c) => int.tryParse(c.text.trim()) ?? 0).toList(); - final center = _targetImageCenterForScoring ?? fixedRingTargetCenter; - - setState(() { - for (final score in scores) { - final offset = _generatePositionForScore(score, center); - _mappedBullets.add(offset); - _manualBulletScores.add(score); - } - }); - - Navigator.pop(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: _bulletColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text(_t('Add Bullets')), - ), - ), - ], - ), - ), - ); - }, - ); -} - - Future _saveTargetData() async { - try { - // Show a dialog to let the user select between a new folder or an existing one - final result = await _showSaveOptionsDialog(); - - if (result == null) return; - - String? selectedFolder; - bool isTournament = false; - try { - final decoded = jsonDecode(result); - selectedFolder = decoded['folder']; - isTournament = decoded['isTournament'] == true; - } catch (_) { - selectedFolder = result; // Treat plain string as folder name (existing folder) + // 1. Load target background image FIRST to calculate scale factors + _targetImageDecoded = await _targetImageFuture; + if (_targetImageDecoded != null) { + _cachedTargetImage = + Uint8List.fromList(img.encodePng(_targetImageDecoded!)); + // Calculate scale factors based on target image dimensions + _calculateScaleFactors(); + } + + // 2. Read the real image file + final File realImageFile = File(widget.imagePath); + final realBytes = await realImageFile.readAsBytes(); + + // 3. Decode the real image (ensure it's valid) + _realImageDecoded = img.decodeImage(realBytes); + if (_realImageDecoded == null) { + debugPrint("❌ Failed to decode image from path: ${widget.imagePath}"); + setState(() => _isLoading = false); + return; + } + + // 4. Cache decoded image as PNG for rendering + _cachedRealImage = Uint8List.fromList(img.encodePng(_realImageDecoded!)); + + // 5. Find the detected ring center and size + final ringCamData = _getRingCenterSize(_realImageDetections); + if (ringCamData == null) { + debugPrint("⚠️ Ring (classId 2) not detected in real image."); + setState(() => _isLoading = false); + return; + } + + final Offset ringCamCenter = ringCamData['center']; + final Size sizeCam = ringCamData['size']; + + // 6. Extract all bullet centers (classId 0) + final List bulletsCam = _realImageDetections + .where((r) => r.classId == 0) + .map((r) => Offset((r.x1 + r.x2) / 2, (r.y1 + r.y2) / 2)) + .toList(); + + // 7. Step 1: Map bullets to DEFAULT target coordinate system (1200x1200) + final List> scoredBullets = bulletsCam.map((bullet) { + final mappedToDefaultTarget = _mapToTargetNormalized( + bullet, + ringCamCenter, + fixedRingTargetCenter, + sizeCam, + fixedRingTargetSize, + ); + final score = _scoreForBullet( + mappedToDefaultTarget, + fixedRingTargetCenter, // Always score relative to default target center + ); + return {'bullet': mappedToDefaultTarget, 'score': score}; + }).toList(); + + // 8. Trim to bullet limit by removing highest scoring ones + scoredBullets.sort((a, b) => b['score'].compareTo(a['score'])); + while (scoredBullets.length > widget.bulletCount) { + scoredBullets.removeAt(0); + } + + // 9. Step 2: Remap bullets from default target to actual display target + var mappedBullets = scoredBullets.map((bulletData) { + final defaultTargetBullet = bulletData['bullet'] as Offset; + // Scale bullet position based on actual target dimensions + final displayBullet = Offset( + defaultTargetBullet.dx * _scaleFactorX, + defaultTargetBullet.dy * _scaleFactorY, + ); + return displayBullet; + }).toList(); + + // 10. Update state with remapped bullets + setState(() { + _mappedBullets = mappedBullets; + _isLoading = false; + }); + } catch (e, stack) { + debugPrint("❌ Error during detection mapping: $e"); + debugPrint(stack.toString()); + setState(() { + _mappedBullets = []; + _isLoading = false; + }); } - - if (selectedFolder == null) return; - - final directory = await getApplicationDocumentsDirectory(); - final saveDirectory = Directory('${directory.path}/saved_images/$selectedFolder'); - - // Create folder if it doesn't exist - if (!await saveDirectory.exists()) { - await saveDirectory.create(recursive: true); - } - - // Save tournament metadata if applicable - if (isTournament) { - final metadataFile = File('${saveDirectory.path}/metadata.json'); - await metadataFile.writeAsString(jsonEncode({'isTournament': true})); - } - - // Use timestamp for a unique file name - final now = DateTime.now(); - final dateString = "${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year}"; - final dateStringFile = "${now.year}-${now.month}-${now.day}"; - final timeString = "${now.hour}-${now.minute}-${now.second}"; - final fileName = "target_${dateStringFile}_$timeString.json"; - final filePath = '${saveDirectory.path}/$fileName'; - - // Prepare target data to save - final targetData = { - 'date': dateString, - 'totalScore': _totalScore, - 'bullets': _mappedBullets.map((b) => {'x': b.dx, 'y': b.dy}).toList(), - }; - - // Write file - final jsonFile = File(filePath); - await jsonFile.writeAsString(jsonEncode(targetData)); - - // Close preview and signal refresh - Navigator.pop(context, true); - } catch (e) { - print("Error saving target data: $e"); } -} - Widget _buildEditController() { - if (!_isEditMode || _selectedBulletIndexInEdit == null) return const SizedBox.shrink(); + void _manualScoreEntry() { + final missingCount = widget.bulletCount - _mappedBullets.length; + final controllers = + List.generate(missingCount, (_) => TextEditingController()); - return Positioned( - bottom: 20, - left: 0, - right: 0, - child: Center( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.75), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Up - IconButton( - icon: const Icon(Icons.keyboard_arrow_up, size: 36, color: Colors.white), - onPressed: () => _moveBullet(const Offset(0, -3)), - splashRadius: 24, - ), - - // Left - Check - Right - Row( + showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.keyboard_arrow_left, size: 36, color: Colors.white), - onPressed: () => _moveBullet(const Offset(-3, 0)), - splashRadius: 24, - ), - const SizedBox(width: 8), - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.greenAccent.withOpacity(0.2), - border: Border.all(color: Colors.greenAccent, width: 2), + Text( + _t('Enter Missing Bullet Scores'), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, ), - child: IconButton( - icon: const Icon(Icons.check, color: Colors.greenAccent, size: 28), - onPressed: () => setState(() => _selectedBulletIndexInEdit = null), - splashRadius: 22, + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: SingleChildScrollView( + child: Column( + children: List.generate(missingCount, (index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: TextField( + controller: controllers[index], + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: + '${_t('Bullet Score')} ${_mappedBullets.length + index + 1}', + labelStyle: + const TextStyle(color: Colors.white54), + filled: true, + fillColor: Colors.grey[800], + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: _bulletColor), + ), + ), + ), + ); + }), + ), ), ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.keyboard_arrow_right, size: 36, color: Colors.white), - onPressed: () => _moveBullet(const Offset(3, 0)), - splashRadius: 24, + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + final scores = controllers + .map((c) => int.tryParse(c.text.trim()) ?? 0) + .toList(); + + setState(() { + for (final score in scores) { + final offset = _generatePositionForScore(score); + // Scale position from default coordinate system to display target size + final scaledOffset = Offset( + offset.dx * _scaleFactorX, + offset.dy * _scaleFactorY, + ); + _mappedBullets.add(scaledOffset); + _manualBulletScores.add(score); + } + }); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _bulletColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text(_t('Add Bullets')), + ), ), ], ), - - // Down - IconButton( - icon: const Icon(Icons.keyboard_arrow_down, size: 36, color: Colors.white), - onPressed: () => _moveBullet(const Offset(0, 3)), - splashRadius: 24, - ), - ], - ), - ), - ), - ); -} - - - -void _moveBullet(Offset offsetDelta) { - setState(() { - final index = _selectedBulletIndexInEdit!; - final current = _mappedBullets[index]; - final updated = current + offsetDelta; - - _mappedBullets[index] = updated; - - // If this is a manually added bullet, update its score live - final manualStartIndex = _mappedBullets.length - _manualBulletScores.length; - if (index >= manualStartIndex) { - final updatedScore = _scoreForBullet( - updated, - _targetImageCenterForScoring ?? fixedRingTargetCenter, - ); - _manualBulletScores[index - manualStartIndex] = updatedScore; - } - }); -} - - - - Future _showSaveOptionsDialog() async { - final directory = await getApplicationDocumentsDirectory(); - final saveDirectory = Directory('${directory.path}/saved_images'); - - if (!await saveDirectory.exists()) { - await saveDirectory.create(recursive: true); + ), + ); + }, + ); } - final folders = saveDirectory.listSync().whereType().toList(); - - // Sort to get most recently modified - folders.sort((a, b) => b.statSync().changed.compareTo(a.statSync().changed)); - final String? lastFolder = folders.isNotEmpty ? folders.first.path.split('/').last : null; - - bool lastIsTournament = false; - - if (lastFolder != null) { - final metadataFile = File('${folders.first.path}/metadata.json'); - if (await metadataFile.exists()) { - try { - final metadata = jsonDecode(await metadataFile.readAsString()); - lastIsTournament = metadata['isTournament'] == true; - } catch (_) {} - } - } - - // Auto-generate next Training and Tournament folder names - int trainingCount = 1; - int tournamentCount = 1; - - // Get localized folder names - final trainingWord = _t('Training'); - final tournamentWord = _t('Tournament'); - - for (var folder in folders) { - final name = folder.path.split('/').last; - // Check for both English and Slovenian names - if (name.startsWith('Training') || name.startsWith('Trening')) { - final num = int.tryParse(name.replaceAll('Training ', '').replaceAll('Trening ', '')); - if (num != null && num >= trainingCount) trainingCount = num + 1; - } - if (name.startsWith('Tournament') || name.startsWith('Tekmovanje')) { - final num = int.tryParse(name.replaceAll('Tournament ', '').replaceAll('Tekmovanje ', '')); - if (num != null && num >= tournamentCount) tournamentCount = num + 1; - } - } - - final String nextTraining = '$trainingWord $trainingCount'; - final String nextTournament = '$tournamentWord $tournamentCount'; - - return showDialog( - context: context, - builder: (context) { - return Dialog( - backgroundColor: Colors.grey[900], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _t('Save Target Data'), - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - - // 🎯 Training Button (New folder) - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop( - context, - jsonEncode({'folder': nextTraining, 'isTournament': false}), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: _bulletColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Column( - children: [ - 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( - _t('New folder'), - style: const TextStyle(fontSize: 12, color: Colors.white70), - ), - ], + void _showMissingBulletDialog(int bulletNum) { + showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${_t('Bullet')} $bulletNum - ${_t('Bullet Score')}', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - ), - const SizedBox(height: 12), - - // πŸ† Tournament Button (New folder) - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pop( - context, - jsonEncode({'folder': nextTournament, 'isTournament': true}), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: _bulletColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Column( - children: [ - 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( - _t('New folder'), - style: const TextStyle(fontSize: 12, color: Colors.white70), - ), - ], - ), - ), - ), - if (lastFolder != null) ...[ - const SizedBox(height: 12), - - // πŸ“ Last Used Folder Button (highlighted subtly) - SizedBox( - width: double.infinity, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.white.withOpacity(0.2), width: 1), - borderRadius: BorderRadius.circular(12), - ), - child: ElevatedButton( + const SizedBox(height: 24), + // Score buttons 1-9 in 3x3 grid + GridView.count( + crossAxisCount: 3, + shrinkWrap: true, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.0, + children: List.generate(9, (scoreIndex) { + final score = 9 - scoreIndex; + return ElevatedButton( onPressed: () { - Navigator.pop( - context, - jsonEncode({ - 'folder': lastFolder, - 'isTournament': lastIsTournament, - }), + final bulletPosition = _generatePositionForScore(score); + // Scale position from default coordinate system to display target size + final scaledBulletPosition = Offset( + bulletPosition.dx * _scaleFactorX, + bulletPosition.dy * _scaleFactorY, ); + + setState(() { + _mappedBullets.add(scaledBulletPosition); + _manualBulletScores.add(score); + _isEditMode = true; + _selectedBulletIndexInEdit = + _mappedBullets.length - 1; + }); + + Navigator.pop(context); }, style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[800], + backgroundColor: _bulletColor, foregroundColor: Colors.white, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), - child: Column( - children: [ - 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), - ), - ], + child: Text( + '$score', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ); + }), + ), + const SizedBox(height: 12), + // Bottom row with 0 and 10 + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + final score = 0; + final bulletPosition = + _generatePositionForScore(score); + // Scale position from default coordinate system to display target size + final scaledBulletPosition = Offset( + bulletPosition.dx * _scaleFactorX, + bulletPosition.dy * _scaleFactorY, + ); + + setState(() { + _mappedBullets.add(scaledBulletPosition); + _manualBulletScores.add(score); + _isEditMode = true; + _selectedBulletIndexInEdit = + _mappedBullets.length - 1; + }); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[600], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - const SizedBox(height: 4), - Text( - _t('Last used folder'), - style: const TextStyle(fontSize: 12, color: Colors.white70), + ), + child: const Text( + '0', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, ), - ], + ), ), ), - ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + final score = 10; + final bulletPosition = + _generatePositionForScore(score); + // Scale position from default coordinate system to display target size + final scaledBulletPosition = Offset( + bulletPosition.dx * _scaleFactorX, + bulletPosition.dy * _scaleFactorY, + ); + + setState(() { + _mappedBullets.add(scaledBulletPosition); + _manualBulletScores.add(score); + _isEditMode = true; + _selectedBulletIndexInEdit = + _mappedBullets.length - 1; + }); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[600], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '10', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], ), ], + ), + ), + ); + }, + ); + } + + Future _saveTargetData() async { + try { + // Show a dialog to let the user select between a new folder or an existing one + final result = await _showSaveOptionsDialog(); + + if (result == null) return; + + String? selectedFolder; + bool isTournament = false; + + try { + final decoded = jsonDecode(result); + selectedFolder = decoded['folder']; + isTournament = decoded['isTournament'] == true; + } catch (_) { + selectedFolder = + result; // Treat plain string as folder name (existing folder) + } + + if (selectedFolder == null) return; + + final directory = await getApplicationDocumentsDirectory(); + final saveDirectory = + Directory('${directory.path}/saved_images/$selectedFolder'); + + // Create folder if it doesn't exist + if (!await saveDirectory.exists()) { + await saveDirectory.create(recursive: true); + } + + // Save tournament metadata if applicable + if (isTournament) { + final metadataFile = File('${saveDirectory.path}/metadata.json'); + await metadataFile.writeAsString(jsonEncode({'isTournament': true})); + } + + // Use timestamp for a unique file name + final now = DateTime.now(); + final dateString = + "${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year}"; + final dateStringFile = "${now.year}-${now.month}-${now.day}"; + final timeString = "${now.hour}-${now.minute}-${now.second}"; + final fileName = "target_${dateStringFile}_$timeString.json"; + final filePath = '${saveDirectory.path}/$fileName'; + + // Prepare target data to save + // Convert bullets from display coordinates back to default target coordinates (1200x1200) + final bulletsInDefaultCoordinates = _mappedBullets.map((b) { + return { + 'x': b.dx / _scaleFactorX, + 'y': b.dy / _scaleFactorY, + }; + }).toList(); + + final targetData = { + 'date': dateString, + 'totalScore': _totalScore, + 'bullets': bulletsInDefaultCoordinates, + }; + + // Write file + final jsonFile = File(filePath); + await jsonFile.writeAsString(jsonEncode(targetData)); + + // Close preview and signal refresh + Navigator.pop(context, true); + } catch (e) { + print("Error saving target data: $e"); + } + } + + Widget _buildEditController() { + if (!_isEditMode || _selectedBulletIndexInEdit == null) + return const SizedBox.shrink(); + + return Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.75), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Up + IconButton( + icon: const Icon(Icons.keyboard_arrow_up, + size: 36, color: Colors.white), + onPressed: () => _moveBullet(const Offset(0, -3)), + splashRadius: 24, + ), + + // Left - Check - Right + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.keyboard_arrow_left, + size: 36, color: Colors.white), + onPressed: () => _moveBullet(const Offset(-3, 0)), + splashRadius: 24, + ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.greenAccent.withValues(alpha: 0.2), + border: Border.all(color: Colors.greenAccent, width: 2), + ), + child: IconButton( + icon: const Icon(Icons.check, + color: Colors.greenAccent, size: 28), + onPressed: () => + setState(() => _selectedBulletIndexInEdit = null), + splashRadius: 22, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.keyboard_arrow_right, + size: 36, color: Colors.white), + onPressed: () => _moveBullet(const Offset(3, 0)), + splashRadius: 24, + ), + ], + ), + + // Down + IconButton( + icon: const Icon(Icons.keyboard_arrow_down, + size: 36, color: Colors.white), + onPressed: () => _moveBullet(const Offset(0, 3)), + splashRadius: 24, + ), ], ), ), - ); - }, - ); -} + ), + ); + } + + void _moveBullet(Offset offsetDelta) { + setState(() { + final index = _selectedBulletIndexInEdit!; + final current = _mappedBullets[index]; + final updated = current + offsetDelta; + + _mappedBullets[index] = updated; + + // If this is a manually added bullet, update its score live + final manualStartIndex = + _mappedBullets.length - _manualBulletScores.length; + if (index >= manualStartIndex) { + // Scale bullet back to default coordinate system for scoring + final defaultTargetBullet = Offset( + updated.dx / _scaleFactorX, + updated.dy / _scaleFactorY, + ); + final updatedScore = _scoreForBullet( + defaultTargetBullet, + fixedRingTargetCenter, + ); + _manualBulletScores[index - manualStartIndex] = updatedScore; + } + }); + } + + Future _showSaveOptionsDialog() async { + final directory = await getApplicationDocumentsDirectory(); + final saveDirectory = Directory('${directory.path}/saved_images'); + + if (!await saveDirectory.exists()) { + await saveDirectory.create(recursive: true); + } + + final folders = saveDirectory.listSync().whereType().toList(); + + // Sort to get most recently modified + folders + .sort((a, b) => b.statSync().changed.compareTo(a.statSync().changed)); + final String? lastFolder = + folders.isNotEmpty ? folders.first.path.split('/').last : null; + + bool lastIsTournament = false; + + if (lastFolder != null) { + final metadataFile = File('${folders.first.path}/metadata.json'); + if (await metadataFile.exists()) { + try { + final metadata = jsonDecode(await metadataFile.readAsString()); + lastIsTournament = metadata['isTournament'] == true; + } catch (_) {} + } + } + + // Auto-generate next Training and Tournament folder names + int trainingCount = 1; + int tournamentCount = 1; + + // Get localized folder names + final trainingWord = _t('Training'); + final tournamentWord = _t('Tournament'); + + for (var folder in folders) { + final name = folder.path.split('/').last; + // Check for both English and Slovenian names + if (name.startsWith('Training') || name.startsWith('Trening')) { + final num = int.tryParse( + name.replaceAll('Training ', '').replaceAll('Trening ', '')); + if (num != null && num >= trainingCount) trainingCount = num + 1; + } + if (name.startsWith('Tournament') || name.startsWith('Tekmovanje')) { + final num = int.tryParse( + name.replaceAll('Tournament ', '').replaceAll('Tekmovanje ', '')); + if (num != null && num >= tournamentCount) tournamentCount = num + 1; + } + } + + final String nextTraining = '$trainingWord $trainingCount'; + final String nextTournament = '$tournamentWord $tournamentCount'; + + return showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _t('Save Target Data'), + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + + // 🎯 Training Button (New folder) + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop( + context, + jsonEncode( + {'folder': nextTraining, 'isTournament': false}), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _bulletColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Column( + children: [ + 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( + _t('New folder'), + style: const TextStyle( + fontSize: 12, color: Colors.white70), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + + // πŸ† Tournament Button (New folder) + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop( + context, + jsonEncode( + {'folder': nextTournament, 'isTournament': true}), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _bulletColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Column( + children: [ + 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( + _t('New folder'), + style: const TextStyle( + fontSize: 12, color: Colors.white70), + ), + ], + ), + ), + ), + if (lastFolder != null) ...[ + const SizedBox(height: 12), + + // πŸ“ Last Used Folder Button (highlighted subtly) + SizedBox( + width: double.infinity, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), width: 1), + borderRadius: BorderRadius.circular(12), + ), + child: ElevatedButton( + onPressed: () { + Navigator.pop( + context, + jsonEncode({ + 'folder': lastFolder, + 'isTournament': lastIsTournament, + }), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[800], + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Column( + children: [ + 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( + _t('Last used folder'), + style: const TextStyle( + fontSize: 12, color: Colors.white70), + ), + ], + ), + ), + ), + ), + ], + ], + ), + ), + ); + }, + ); + } Map? _getRingCenterSize(List results) { try { @@ -838,22 +1228,27 @@ void _moveBullet(Offset offsetDelta) { } } - Offset _mapToTargetNormalized( - Offset bulletCam, - Offset ringCam, - Offset ringTarget, - Size sizeCam, - Size sizeTarget) { - final normX = (bulletCam.dx - ringCam.dx) / sizeCam.width; - final normY = (bulletCam.dy - ringCam.dy) / sizeCam.height; - final mappedX = normX * sizeTarget.width + ringTarget.dx; - final mappedY = normY * sizeTarget.height + ringTarget.dy; + Offset _mapToTargetNormalized(Offset bulletCam, Offset ringCam, + Offset ringTarget, Size sizeCam, Size sizeTarget) { + // Calculate scale factor between detected ring and target ring + final scaleX = sizeTarget.width / sizeCam.width; + final scaleY = sizeTarget.height / sizeCam.height; + + // Translate bullet position relative to ring center + final relativeX = bulletCam.dx - ringCam.dx; + final relativeY = bulletCam.dy - ringCam.dy; + + // Scale the relative position and map to target + final mappedX = (relativeX * scaleX) + ringTarget.dx; + final mappedY = (relativeY * scaleY) + ringTarget.dy; + return Offset(mappedX, mappedY); } int _scoreForBullet(Offset bullet, Offset centerOverride) { const double bulletRadius = 25; - final double distanceToEdge = (bullet - centerOverride).distance - bulletRadius; + final double distanceToEdge = + (bullet - centerOverride).distance - bulletRadius; final List rings = [14, 70, 125, 180, 235, 290, 345, 400, 455, 510]; @@ -867,402 +1262,584 @@ void _moveBullet(Offset offsetDelta) { } int get _totalScore { - final autoScore = _mappedBullets - .take(widget.bulletCount - _manualBulletScores.length) - .map((b) => _scoreForBullet(b, _targetImageCenterForScoring ?? fixedRingTargetCenter)) - .fold(0, (sum, score) => sum + score); + // Score is based on the default target coordinate system (1200x1200) + // since bullets were mapped to that system and then scaled for display + final autoScore = _mappedBullets + .take(widget.bulletCount - _manualBulletScores.length) + .map((b) { + // Scale bullet back to default coordinate system for scoring + final defaultTargetBullet = Offset( + b.dx / _scaleFactorX, + b.dy / _scaleFactorY, + ); + return _scoreForBullet(defaultTargetBullet, fixedRingTargetCenter); + }).fold(0, (sum, score) => sum + score); - final manualScore = _manualBulletScores.fold(0, (a, b) => a + b); + final manualScore = _manualBulletScores.fold(0, (a, b) => a + b); - return autoScore + manualScore; -} + return autoScore + manualScore; + } -Color _getBulletButtonColor(int index) { - final isSelected = _isEditMode - ? index == _selectedBulletIndexInEdit - : index == _highlightedBulletIndex; + Color _getBulletButtonColor(int index) { + final isSelected = _isEditMode + ? index == _selectedBulletIndexInEdit + : index == _highlightedBulletIndex; - if (isSelected) return Colors.red; + if (isSelected) return Colors.red; - final manualStart = _mappedBullets.length - _manualBulletScores.length; - final isManual = index >= manualStart; + final manualStart = _mappedBullets.length - _manualBulletScores.length; + final isManual = index >= manualStart; - if (isManual) return Colors.redAccent; // ⬅️ Light red for manual bullets + if (isManual) return Colors.redAccent; // ⬅️ Light red for manual bullets - if (_isEditMode) return _editColor; - - return _bulletColor; -} + if (_isEditMode) return _editColor; + return _bulletColor; + } @override -Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _backgroundColor, - appBar: AppBar( - backgroundColor: Colors.grey[900], - iconTheme: const IconThemeData(color: Colors.white), - title: Text( - _t('Detection Preview'), - style: const TextStyle(color: Colors.white), - ), - actions: [ - IconButton( - icon: Icon( - _mode == PreviewMode.mappedTarget - ? Icons.camera_alt - : Icons.center_focus_strong, + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _backgroundColor, + appBar: AppBar( + backgroundColor: Colors.grey[900], + iconTheme: const IconThemeData(color: Colors.white), + title: Text( + _t('Detection Preview'), + style: const TextStyle(color: Colors.white), + ), + actions: [ + IconButton( + icon: Icon( + _mode == PreviewMode.mappedTarget + ? Icons.camera_alt + : Icons.center_focus_strong, + ), + color: Colors.white, + onPressed: () async { + setState(() { + _mode = _mode == PreviewMode.mappedTarget + ? PreviewMode.cameraWithDetections + : PreviewMode.mappedTarget; + }); + await _loadSelectedTarget(); // Reload target based on new mode + }, + tooltip: _mode == PreviewMode.mappedTarget + ? _t("Show Camera Detections") + : _t("Show Mapped Target"), ), - color: Colors.white, - onPressed: () async { - setState(() { - _mode = _mode == PreviewMode.mappedTarget - ? PreviewMode.cameraWithDetections - : PreviewMode.mappedTarget; - }); - await _loadSelectedTarget(); // Reload target based on new mode - }, - tooltip: _mode == PreviewMode.mappedTarget - ? _t("Show Camera Detections") - : _t("Show Mapped Target"), - ), - IconButton( - icon: const Icon(Icons.save), - color: Colors.white, - onPressed: _saveTargetData, - tooltip: _t("Save Target Data"), - ), - ], - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : Stack( - children: [ - Column( - children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final screenWidth = constraints.maxWidth; - final screenHeight = constraints.maxHeight; + IconButton( + icon: const Icon(Icons.save), + color: Colors.white, + onPressed: _saveTargetData, + tooltip: _t("Save Target Data"), + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Stack( + children: [ + Column( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final screenWidth = constraints.maxWidth; + final screenHeight = constraints.maxHeight; - final displayImage = _mode == PreviewMode.mappedTarget - ? _cachedTargetImage - : _cachedRealImage; + final displayImage = _mode == PreviewMode.mappedTarget + ? _cachedTargetImage + : _cachedRealImage; - final imgData = _mode == PreviewMode.mappedTarget - ? _targetImageDecoded - : _realImageDecoded; + final imgData = _mode == PreviewMode.mappedTarget + ? _targetImageDecoded + : _realImageDecoded; - if (displayImage == null || imgData == null) { - return const Center(child: Text("Image not available")); - } + if (displayImage == null || imgData == null) { + return const Center( + child: Text("Image not available")); + } - final imgWidth = imgData.width.toDouble(); - final imgHeight = imgData.height.toDouble(); - final aspectRatio = imgWidth / imgHeight; - final screenAspect = screenWidth / screenHeight; + final imgWidth = imgData.width.toDouble(); + final imgHeight = imgData.height.toDouble(); + final aspectRatio = imgWidth / imgHeight; + final screenAspect = screenWidth / screenHeight; - double displayWidth, displayHeight, offsetX, offsetY; + double displayWidth, displayHeight, offsetX, offsetY; - if (aspectRatio > screenAspect) { - displayWidth = screenWidth; - displayHeight = screenWidth / aspectRatio; - offsetX = 0; - offsetY = (screenHeight - displayHeight) / 2; - } else { - displayHeight = screenHeight; - displayWidth = screenHeight * aspectRatio; - offsetY = 0; - offsetX = (screenWidth - displayWidth) / 2; - } + if (aspectRatio > screenAspect) { + displayWidth = screenWidth; + displayHeight = screenWidth / aspectRatio; + offsetX = 0; + offsetY = (screenHeight - displayHeight) / 2; + } else { + displayHeight = screenHeight; + displayWidth = screenHeight * aspectRatio; + offsetY = 0; + offsetX = (screenWidth - displayWidth) / 2; + } - final scaleX = displayWidth / imgWidth; - final scaleY = displayHeight / imgHeight; + final scaleX = displayWidth / imgWidth; + final scaleY = displayHeight / imgHeight; - final imageWidget = Image.memory(displayImage, fit: BoxFit.fill); + final imageWidget = + Image.memory(displayImage, fit: BoxFit.fill); - return Stack( - children: [ - Positioned( - left: offsetX, - top: offsetY, - width: displayWidth, - height: displayHeight, - child: imageWidget, - ), - if (_mode == PreviewMode.cameraWithDetections) - ..._realImageDetections.map((d) { - final left = d.x1 * scaleX + offsetX; - final top = d.y1 * scaleY + offsetY; - final width = (d.x2 - d.x1) * scaleX; - final height = (d.y2 - d.y1) * scaleY; - return Positioned( - left: left, - top: top, - width: width, - height: height, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.green, width: 2), - ), - child: Align( - alignment: Alignment.topLeft, - child: Container( - color: Colors.green.withOpacity(0.5), - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: Text( - "ID:${d.classId} ${(d.confidence * 100).toStringAsFixed(1)}%", - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), + return Stack( + children: [ + Positioned( + left: offsetX, + top: offsetY, + width: displayWidth, + height: displayHeight, + child: imageWidget, + ), + if (_mode == PreviewMode.cameraWithDetections) + ..._realImageDetections.map((d) { + final left = d.x1 * scaleX + offsetX; + final top = d.y1 * scaleY + offsetY; + final width = (d.x2 - d.x1) * scaleX; + final height = (d.y2 - d.y1) * scaleY; + return Positioned( + left: left, + top: top, + width: width, + height: height, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.green, width: 2), ), - ), - ), - ); - }).toList(), - if (_mode == PreviewMode.mappedTarget) - ..._mappedBullets.asMap().entries.take(widget.bulletCount).map((entry) { - final index = entry.key; - final b = entry.value; - final dx = b.dx * scaleX + offsetX; - final dy = b.dy * scaleY + offsetY; - final double bulletSize = ((scaleX + scaleY) / 2) * 55; - - final isHighlighted = index == _highlightedBulletIndex; - final isSelectedForEdit = index == _selectedBulletIndexInEdit; - final manualStart = _mappedBullets.length - _manualBulletScores.length; - final isManual = index >= manualStart; - - final Color bulletFillColor = isSelectedForEdit || (!_isEditMode && isHighlighted) - ? Colors.red.withOpacity(_bulletOpacity) - : isManual - ? Colors.red.withOpacity(_bulletOpacity) - : _activeColorWithOpacity; - - return Positioned( - left: dx - bulletSize / 2, - top: dy - bulletSize / 2, - child: Container( - width: bulletSize, - height: bulletSize, - decoration: BoxDecoration( - color: bulletFillColor, - shape: BoxShape.circle, - border: Border.all( - color: (isSelectedForEdit || (!_isEditMode && isHighlighted)) - ? Colors.red - : Colors.black, - width: (isSelectedForEdit || (!_isEditMode && isHighlighted)) ? 3 : 1, - ), - ), - ), - ); - }).toList(), - ], - ); - }, - ), - ), - const SizedBox(height: 12), - if (_mappedBullets.isNotEmpty) - Column( - children: [ - // Score buttons - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: AbsorbPointer( - absorbing: _isEditMode && _selectedBulletIndexInEdit != null, - child: Row( - children: List.generate( - _mappedBullets.length, - (index) { - final bullet = _mappedBullets[index]; - final isManual = index >= (_mappedBullets.length - _manualBulletScores.length); - final score = isManual - ? _manualBulletScores[index - (_mappedBullets.length - _manualBulletScores.length)] - : _scoreForBullet( - bullet, - _targetImageCenterForScoring ?? fixedRingTargetCenter, - ); - - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: GestureDetector( - onTap: () { - if (_isEditMode) { - setState(() => _selectedBulletIndexInEdit = index); - } - }, - onTapDown: (_) { - if (!_isEditMode) { - setState(() => _highlightedBulletIndex = index); - } - }, - onTapUp: (_) { - if (!_isEditMode) { - setState(() => _highlightedBulletIndex = null); - } - }, - onTapCancel: () { - if (!_isEditMode) { - setState(() => _highlightedBulletIndex = null); - } - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - height: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - offset: Offset(0, 2), - blurRadius: 6, - ), - ], - ), + child: Align( + alignment: Alignment.topLeft, 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: 12, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - Text( - '$score', - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ], + color: Colors.green.withValues(alpha: 0.5), + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), + child: Text( + "ID:${d.classId} ${(d.confidence * 100).toStringAsFixed(1)}%", + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), ), ), ), + ); + }).toList(), + // DEBUG: Show scoring rings in both modes to verify alignment + if (_DEBUG_SHOW_SCORING_RINGS) + Positioned( + left: offsetX, + top: offsetY, + width: displayWidth, + height: displayHeight, + child: CustomPaint( + painter: ScoringRingsPainter( + center: fixedRingTargetCenter, + scaleX: scaleX, + scaleY: scaleY, + targetScaleFactorX: _scaleFactorX, + targetScaleFactorY: _scaleFactorY, + ), ), - ); - }, + ), + if (_mode == PreviewMode.mappedTarget) + ..._mappedBullets + .asMap() + .entries + .take(widget.bulletCount) + .map((entry) { + final index = entry.key; + final b = entry.value; + final dx = b.dx * scaleX + offsetX; + final dy = b.dy * scaleY + offsetY; + // Scale bullet size by target scale factors and display scale factors + final double bulletSize = + (((_scaleFactorX + _scaleFactorY) / 2) * + 53) * + ((scaleX + scaleY) / 2); + + final isHighlighted = + index == _highlightedBulletIndex; + final isSelectedForEdit = + index == _selectedBulletIndexInEdit; + final manualStart = _mappedBullets.length - + _manualBulletScores.length; + final isManual = index >= manualStart; + + final Color bulletFillColor = + isSelectedForEdit || + (!_isEditMode && isHighlighted) + ? Colors.red + .withValues(alpha: _bulletOpacity) + : isManual + ? Colors.red + .withValues(alpha: _bulletOpacity) + : _activeColorWithOpacity; + + return Positioned( + left: dx - bulletSize / 2, + top: dy - bulletSize / 2, + child: Container( + width: bulletSize, + height: bulletSize, + decoration: BoxDecoration( + color: bulletFillColor, + shape: BoxShape.circle, + border: Border.all( + color: (isSelectedForEdit || + (!_isEditMode && + isHighlighted)) + ? Colors.red + : Colors.black, + width: (isSelectedForEdit || + (!_isEditMode && + isHighlighted)) + ? 3 + : 1, + ), + ), + ), + ); + }).toList(), + ], + ); + }, + ), + ), + const SizedBox(height: 12), + if (_mappedBullets.isNotEmpty) + Column( + children: [ + // Score buttons for detected and missing bullets + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + child: AbsorbPointer( + absorbing: _isEditMode && + _selectedBulletIndexInEdit != null, + child: Row( + children: [ + // Detected bullets + ...List.generate( + _mappedBullets.length, + (index) { + final bullet = _mappedBullets[index]; + final isManual = index >= + (_mappedBullets.length - + _manualBulletScores.length); + final score = isManual + ? _manualBulletScores[index - + (_mappedBullets.length - + _manualBulletScores.length)] + : _scoreForBullet( + // Convert bullet back to default coordinate system for scoring + Offset( + bullet.dx / _scaleFactorX, + bullet.dy / _scaleFactorY, + ), + fixedRingTargetCenter, + ); + + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6), + child: GestureDetector( + onTap: () { + if (_isEditMode) { + setState(() => + _selectedBulletIndexInEdit = + index); + } + }, + onTapDown: (_) { + if (!_isEditMode) { + setState(() => + _highlightedBulletIndex = + index); + } + }, + onTapUp: (_) { + if (!_isEditMode) { + setState(() => + _highlightedBulletIndex = + null); + } + }, + onTapCancel: () { + if (!_isEditMode) { + setState(() => + _highlightedBulletIndex = + null); + } + }, + child: AnimatedContainer( + duration: const Duration( + milliseconds: 200), + height: 100, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + offset: Offset(0, 2), + blurRadius: 6, + ), + ], + ), + 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( + '${index + 1}', + style: const TextStyle( + fontWeight: + FontWeight.bold, + fontSize: 14, + color: Colors.black87, + ), + ), + ), + 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, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + // Missing bullets + ...List.generate( + widget.bulletCount - _mappedBullets.length, + (index) { + final bulletNum = + _mappedBullets.length + index + 1; + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6), + child: GestureDetector( + onTap: () { + _showMissingBulletDialog( + bulletNum); + }, + child: Container( + height: 100, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + offset: Offset(0, 2), + blurRadius: 6, + ), + ], + ), + 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( + '$bulletNum', + style: const TextStyle( + fontWeight: + FontWeight.bold, + fontSize: 14, + color: Colors.black87, + ), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: + const BorderRadius + .only( + bottomLeft: + Radius.circular( + 16), + bottomRight: + Radius.circular( + 16), + ), + ), + alignment: + Alignment.center, + child: const Text( + '?', + style: TextStyle( + fontSize: 32, + fontWeight: + FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ], + ), ), ), + ], + ), + const SizedBox(height: 12), + ], + ), + _buildEditController(), + // Only show edit button if all bullets are detected + if (_mappedBullets.isNotEmpty && + _mappedBullets.length >= widget.bulletCount) + Positioned( + top: 16, + right: 16, + child: GestureDetector( + onTap: () { + setState(() { + _isEditMode = !_isEditMode; + _selectedBulletIndexInEdit = null; + }); + }, + child: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: _isEditMode ? Colors.red : _bulletColor, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + offset: Offset(0, 3), + blurRadius: 6, + ), + ], + ), + child: Icon( + _isEditMode ? Icons.check : Icons.edit, + color: Colors.white, + size: 28, + ), + ), + ), + ), + ], + ), + bottomNavigationBar: BottomAppBar( + color: Colors.grey[900], + padding: EdgeInsets.zero, + child: SizedBox( + height: 150, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Left side - Label with icon + Padding( + padding: const EdgeInsets.only(right: 16), + child: FittedBox( + fit: BoxFit.scaleDown, + 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, ), ), ], ), - const SizedBox(height: 12), - ], + ), ), - _buildEditController(), - if (_mappedBullets.isNotEmpty) - Positioned( - top: 16, - right: 16, - child: GestureDetector( - onTap: () async { - if (!_isEditMode && _mappedBullets.length < widget.bulletCount) { - _manualScoreEntry(); - return; - } - - setState(() { - _isEditMode = !_isEditMode; - _selectedBulletIndexInEdit = null; - }); - }, - child: Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: _isEditMode - ? Colors.red - : (_mappedBullets.length < widget.bulletCount ? Colors.redAccent : _bulletColor), - borderRadius: BorderRadius.circular(16), // πŸ‘ˆ Rounded square - boxShadow: const [ - BoxShadow( - color: Colors.black26, - offset: Offset(0, 3), - blurRadius: 6, - ), - ], - ), - child: Icon( - _isEditMode - ? Icons.check - : (_mappedBullets.length < widget.bulletCount - ? Icons.warning_amber_rounded - : Icons.edit), - color: Colors.white, - size: 28, - ), + // Right side - Score number (no box) + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "$_totalScore", + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), ), - ], - ), - bottomNavigationBar: BottomAppBar( - color: Colors.grey[900], - padding: EdgeInsets.zero, - child: SizedBox( - height: 150, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Left side - Label with icon - Padding( - padding: const EdgeInsets.only(right: 16), - child: FittedBox( - fit: BoxFit.scaleDown, - 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 number (no box) - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "$_totalScore", - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ], + ), ), ), - ), - ); -} - + ); + } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 299dac1..880ed70 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -76,6 +76,12 @@ class _SettingsScreenState extends State { 'assets/targets/Target22.jpg', 'assets/targets/Target23.jpg', 'assets/targets/Target24.jpg', + 'assets/targets/Target25.jpg', + 'assets/targets/Target26.jpg', + 'assets/targets/Target27.jpg', + 'assets/targets/Target28.jpg', + 'assets/targets/Target29.jpg', + 'assets/targets/Target30.jpg', ]; @override @@ -374,9 +380,7 @@ String _getTargetDisplayName(String targetPath) { const SizedBox(height: 8), _buildLanguageSelector(), const SizedBox(height: 16), - _buildBulletColorSelector(), - const SizedBox(height: 16), - _buildTargetSelector(), + _buildAppearanceSelector(), const SizedBox(height: 16), _buildCameraList(), @@ -397,13 +401,6 @@ String _getTargetDisplayName(String targetPath) { 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, @@ -477,177 +474,161 @@ String _getTargetDisplayName(String targetPath) { ); } - 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( + Widget _buildAppearanceSelector() { + return Container( + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: Colors.grey[300], + color: Colors.grey[900], borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(12), - bottomRight: Radius.circular(12), + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), child: Row( children: [ Container( - width: 16, + width: 4, height: 16, - margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( - color: _getColorFromName(_selectedBulletColor), - shape: BoxShape.circle, - border: Border.all(color: Colors.black26), + color: Colors.white, + borderRadius: BorderRadius.circular(2), ), ), - Expanded( - child: Text( - _t(_selectedBulletColor), - style: const TextStyle(color: Colors.black87, fontSize: 14), + const SizedBox(width: 12), + Text( + _t("Appearance"), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), - const Icon(Icons.chevron_right, color: Colors.black54), ], ), ), - ), - ], - ), - ); -} - -Widget _buildTargetSelector() { - 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), + // Content + GestureDetector( + onTap: _showAppearancePopup, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), ), ), - const SizedBox(width: 12), - Text( - _t("Select Target"), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ], - ), - ), - // Content - GestureDetector( - onTap: _showTargetPopup, - 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: [ - Expanded( - child: Text( - _getTargetDisplayName(_selectedTarget), - style: const TextStyle(color: Colors.black87, fontSize: 14), + child: Row( + children: [ + // Preview box with target + Container( + width: 70, + height: 70, + decoration: BoxDecoration( + border: Border.all(color: Colors.black12), + borderRadius: BorderRadius.circular(8), + color: TargetColors.getBackgroundColor(_selectedTarget), + ), + child: Image.asset( + _selectedTarget, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Icon(Icons.image_not_supported, color: Colors.grey[400]); + }, + ), ), - ), - const Icon(Icons.chevron_right, color: Colors.black54), - ], + const SizedBox(width: 16), + // Details section + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + _t("Bullet:"), + style: const TextStyle( + color: Colors.black54, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: _getColorFromName(_selectedBulletColor), + shape: BoxShape.circle, + border: Border.all(color: Colors.black26, width: 1), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + _selectedBulletColor, + style: const TextStyle( + color: Colors.black87, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + _t("Target:"), + style: const TextStyle( + color: Colors.black54, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _getTargetDisplayName(_selectedTarget), + style: const TextStyle( + color: Colors.black87, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 8), + Icon(Icons.chevron_right, color: Colors.black54), + ], + ), ), ), - ), - ], - ), - ); -} + ], + ), + ); + } void _showLanguagePopup() { showDialog( @@ -725,192 +706,285 @@ void _showLanguagePopup() { ); } -void _showBulletColorPopup() { - showDialog( - context: context, - builder: (context) { - return Dialog( - backgroundColor: Colors.grey[900], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _t("Select Bullet Color"), - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, + void _showAppearancePopup() { + String previewBulletColor = _selectedBulletColor; + String previewTarget = _selectedTarget; + int selectedTab = 0; // 0 for Bullet, 1 for Target + + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, innerSetState) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + insetPadding: const EdgeInsets.all(24), + child: SizedBox( + height: 700, + width: 400, + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text( + _t("Appearance Settings"), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 20), + + // Preview with multiple bullets on target - ALWAYS ON TOP + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + height: 280, + decoration: BoxDecoration( + border: Border.all(color: Colors.white24), + color: TargetColors.getBackgroundColor(previewTarget), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Center( + child: Image.asset( + previewTarget, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[800], + child: const Center( + child: Icon(Icons.image_not_supported, color: Colors.white54), + ), + ); + }, + ), + ), + // Multiple preview bullets scattered around + Positioned( + left: 30, + top: 40, + child: _buildPreviewBullet(previewBulletColor), + ), + Positioned( + right: 25, + top: 50, + child: _buildPreviewBullet(previewBulletColor), + ), + Positioned( + left: 40, + bottom: 30, + child: _buildPreviewBullet(previewBulletColor), + ), + Positioned( + right: 35, + bottom: 40, + child: _buildPreviewBullet(previewBulletColor), + ), + Positioned( + left: 50, + top: 100, + child: _buildPreviewBullet(previewBulletColor), + ), + Positioned( + right: 50, + bottom: 80, + child: _buildPreviewBullet(previewBulletColor), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Tab Buttons and Content as one unit + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + // Selection Buttons (Tabs) + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => innerSetState(() => selectedTab = 0), + style: ElevatedButton.styleFrom( + backgroundColor: selectedTab == 0 + ? _getColorFromName(previewBulletColor) + : Colors.grey[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + ), + ), + ), + child: Text( + _t("Bullet"), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Expanded( + child: ElevatedButton( + onPressed: () => innerSetState(() => selectedTab = 1), + style: ElevatedButton.styleFrom( + backgroundColor: selectedTab == 1 + ? _getColorFromName(previewBulletColor) + : Colors.grey[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + ), + ), + ), + child: Text( + _t("Target"), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + // Tab Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: selectedTab == 0 + ? Center( + child: Wrap( + spacing: 20, + runSpacing: 20, + alignment: WrapAlignment.center, + children: _bulletColors.map((colorName) { + final color = _getColorFromName(colorName); + final isSelected = colorName == previewBulletColor; + return GestureDetector( + onTap: () => innerSetState(() => previewBulletColor = colorName), + child: Container( + width: 62, + height: 62, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.white : Colors.white24, + width: isSelected ? 4 : 2, + ), + ), + ), + ); + }).toList(), + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: _availableTargets.map((targetPath) { + final isSelected = targetPath == previewTarget; + final displayName = _getTargetDisplayName(targetPath); + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => innerSetState(() => previewTarget = targetPath), + style: ElevatedButton.styleFrom( + backgroundColor: isSelected ? _getColorFromName(previewBulletColor) : Colors.grey[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text( + displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + // Action Buttons + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + await _saveBulletColor(previewBulletColor); + await _saveSelectedTarget(previewTarget); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: _getColorFromName(previewBulletColor), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text(_t("Apply")), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(_t("Cancel"), style: const TextStyle(color: Colors.white70)), + ), + ), + ], + ), + ], + ), ), ), - const SizedBox(height: 20), - Wrap( - spacing: 12, - runSpacing: 12, - children: _bulletColors.map((colorName) { - final color = _getColorFromName(colorName); - final isSelected = colorName == _selectedBulletColor; + ); + }, + ); + }, + ); + } - return GestureDetector( - onTap: () async { - await _saveBulletColor(colorName); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - border: Border.all( - color: isSelected ? Colors.white : Colors.white24, - width: isSelected ? 3 : 1, - ), - ), - ), - ); - }).toList(), - ), - const SizedBox(height: 20), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(_t("Cancel"), style: const TextStyle(color: Colors.white70)), - ), - ], - ), + Widget _buildPreviewBullet(String bulletColor) { + const double bulletOpacity = 0.8; + return Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: _getColorFromName(bulletColor).withValues(alpha: bulletOpacity), + shape: BoxShape.circle, + border: Border.all( + color: Colors.black, + width: 1, ), - ); - }, - ); -} - -void _showTargetPopup() { - String previewTarget = _selectedTarget; - - showDialog( - context: context, - builder: (context) { - return StatefulBuilder( - builder: (context, innerSetState) { - - return Dialog( - backgroundColor: Colors.grey[900], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _t("Select Target"), - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 20), - // Image Preview - Container( - width: 200, - height: 200, - decoration: BoxDecoration( - border: Border.all(color: Colors.white24), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset( - previewTarget, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey[800], - child: const Center( - child: Icon(Icons.image_not_supported, color: Colors.white54), - ), - ); - }, - ), - ), - const SizedBox(height: 20), - // Target List - SizedBox( - width: double.maxFinite, - height: 300, - child: ListView( - children: _availableTargets.map((targetPath) { - final isSelected = targetPath == previewTarget; - final displayName = _getTargetDisplayName(targetPath); - - return Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - innerSetState(() => previewTarget = targetPath); - }, - style: ElevatedButton.styleFrom( - backgroundColor: isSelected - ? _getColorFromName(_selectedBulletColor) - : Colors.grey[800], - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text(displayName), - ), - ), - ); - }).toList(), - ), - ), - const SizedBox(height: 12), - // Action Buttons - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () async { - await _saveSelectedTarget(previewTarget); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: _getColorFromName(_selectedBulletColor), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text(_t("Apply")), - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(_t("Cancel"), style: const TextStyle(color: Colors.white70)), - ), - ), - ], - ), - ], - ), - ), - ); - }, - ); - }, - ); -} + ), + ); + } Widget _buildCameraList() { return Container( @@ -918,13 +992,6 @@ void _showTargetPopup() { 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, diff --git a/lib/screens/spectator_screen.dart b/lib/screens/spectator_screen.dart index a3f2705..48ff921 100644 --- a/lib/screens/spectator_screen.dart +++ b/lib/screens/spectator_screen.dart @@ -20,22 +20,11 @@ class _SpectatorScreenState extends State { List _enabledCameras = List.generate(6, (_) => false); // Which cameras have IPs configured List> _cameras = []; Color _bulletColor = Colors.blue; // default/fallback bullet color - @override void initState() { super.initState(); _loadCameras(); _loadBulletColor(); - _setSystemUIMode(); - } - - @override - void dispose() { - super.dispose(); - } - - void _setSystemUIMode() { - // No special system UI mode needed for portrait } String _t(String key) { @@ -94,110 +83,181 @@ class _SpectatorScreenState extends State { return Colors.pink; case 'lime': return Colors.lime; + case 'mint': + return Colors.greenAccent; case 'orange': return Colors.orange; - case 'red': - return Colors.red; + case 'violet': + return Colors.deepPurpleAccent; + case 'amber': + return Colors.amber; + case 'brown': + return Colors.brown; + case 'grey': + return Colors.grey; default: return Colors.blue; } } void _showCameraSelection() { - showModalBottomSheet( + showDialog( context: context, - backgroundColor: Colors.grey[900], - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), builder: (context) => StatefulBuilder( - builder: (context, setModalState) => SingleChildScrollView( + builder: (context, setModalState) => Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), child: Container( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + padding: const EdgeInsets.fromLTRB(24, 20, 24, 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - _t('Select Cameras'), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, + Text( + _t('Select Cameras'), + style: const TextStyle( + color: Colors.white, + fontSize: 18, + 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]; + const SizedBox(height: 24), + // Row 1: Cameras 1-3 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(3, (index) { + final cameraNum = index + 1; + final isSelected = _selectedCameras[index]; + final isEnabled = _enabledCameras[index]; - return GestureDetector( - onTap: isEnabled - ? () { - setModalState(() { + return GestureDetector( + onTap: isEnabled + ? () { + // Update both modal and parent state simultaneously _selectedCameras[index] = !_selectedCameras[index]; - }); - // Also update the parent state to refresh camera grid - if (mounted) { - setState(() {}); + setModalState(() {}); + 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( + : null, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( color: !isEnabled - ? Colors.grey[600]! - : (isSelected ? _bulletColor : Colors.grey[700]!), - width: 2, + ? 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, + child: Center( + child: Text( + '$cameraNum', + style: const TextStyle( + color: Colors.white, + 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), + ); + }), + ), + const SizedBox(height: 16), + // Row 2: Cameras 4-6 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(3, (index) { + final cameraNum = index + 4; + final arrayIndex = index + 3; + final isSelected = _selectedCameras[arrayIndex]; + final isEnabled = _enabledCameras[arrayIndex]; + + return GestureDetector( + onTap: isEnabled + ? () { + // Update both modal and parent state simultaneously + _selectedCameras[arrayIndex] = !_selectedCameras[arrayIndex]; + setModalState(() {}); + 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: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + }), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Colors.grey[700]!, width: 1), ), ), - onPressed: () => Navigator.pop(context), - child: Text( - _t('Done'), - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 40, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _bulletColor, + 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, + ), + ), + ), + ), + ], ), ), - ), - ], + ], ), ), ), @@ -205,7 +265,6 @@ class _SpectatorScreenState extends State { ); } - @override Widget build(BuildContext context) { final visibleCameras = >[]; @@ -215,6 +274,7 @@ class _SpectatorScreenState extends State { } } + // Portrait list view return Scaffold( backgroundColor: Colors.black, appBar: AppBar( @@ -280,33 +340,31 @@ class _SpectatorScreenState extends State { stream: streamUrl, isLive: true, fit: BoxFit.cover, - error: (context, error, stackTrace) { - return Container( - color: Colors.grey[700], - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.videocam_off, + error: (context, error, stackTrace) => 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, - size: 32, + fontSize: 14, + fontWeight: FontWeight.w600, ), - const SizedBox(height: 8), - Text( - "${_t('Camera')} $cameraIndex", - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - ], - ), + textAlign: TextAlign.center, + ), + ], ), - ); - }, + ), + ), ), // Camera number circle - top right (with bullet color) Positioned( diff --git a/lib/screens/target_view.dart b/lib/screens/target_view.dart index 520f093..be95812 100644 --- a/lib/screens/target_view.dart +++ b/lib/screens/target_view.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:image/image.dart' as img; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/translations.dart'; +import '../utils/target_colors.dart'; class TargetViewScreen extends StatefulWidget { final List jsonFiles; @@ -37,7 +38,6 @@ class _TargetViewScreenState extends State { late String _targetFilePath; late String targetAssetPath; late String _selectedTargetPath; // For display - final double bulletSize = 19; double _scaleFactorX = 1.0; // Scale factor for X coordinate double _scaleFactorY = 1.0; // Scale factor for Y coordinate @@ -45,7 +45,6 @@ class _TargetViewScreenState extends State { String _selectedLanguage = 'English'; Color _backgroundColor = Colors.white; - @override void initState() { super.initState(); @@ -75,7 +74,8 @@ class _TargetViewScreenState extends State { Future _loadSelectedTarget() async { final prefs = await SharedPreferences.getInstance(); - final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png'; + final selectedTarget = + prefs.getString('selectedTarget') ?? 'assets/target.png'; // Always use default target for bullet positioning targetAssetPath = 'assets/target.png'; // Store selected target for display @@ -84,110 +84,54 @@ class _TargetViewScreenState extends State { } Color _getBackgroundColorForTarget(String targetPath) { - // Map each target to its background color (extracted from first pixel of each image) - switch (targetPath) { - case 'assets/target.png': - return const Color(0xFFFFFFFF); // White - case 'assets/targets/Target1.jpg': - return const Color(0xFFE8ECD5); // Light beige - case 'assets/targets/Target2.jpg': - return const Color(0xFFC1DF87); // Light green - case 'assets/targets/Target3.jpg': - return const Color(0xFFFEFEFE); // Off-white - case 'assets/targets/Target4.jpg': - return const Color(0xFFD42931); // Red - case 'assets/targets/Target5.jpg': - return const Color(0xFF02050A); // Very dark blue/black - case 'assets/targets/Target6.jpg': - return const Color(0xFFF3E7D1); // Light beige - case 'assets/targets/Target7.jpg': - return const Color(0xFFFFD454); // Bright yellow - case 'assets/targets/Target8.jpg': - return const Color(0xFF1E5E81); // Dark teal blue - case 'assets/targets/Target9.jpg': - return const Color(0xFFFEFEFC); // Off-white - case 'assets/targets/Target10.jpg': - return const Color(0xFF98926E); // Olive green - case 'assets/targets/Target11.jpg': - return const Color(0xFF13132B); // Dark blue - case 'assets/targets/Target12.jpg': - return const Color(0xFFF5F4EF); // Off-white - case 'assets/targets/Target13.jpg': - return const Color(0xFF174AA5); // Blue - case 'assets/targets/Target14.jpg': - return const Color(0xFFFEE05E); // Light yellow - case 'assets/targets/Target15.jpg': - return const Color(0xFFFFFFFF); // White - case 'assets/targets/Target16.jpg': - return const Color(0xFF000000); // Black - case 'assets/targets/Target17.jpg': - return const Color(0xFF13132B); // Dark blue - case 'assets/targets/Target18.jpg': - return const Color(0xFF050608); // Very dark gray - case 'assets/targets/Target19.jpg': - return const Color(0xFFC32534); // Dark red - case 'assets/targets/Target20.jpg': - return const Color(0xFFFCF3EE); // Light beige - case 'assets/targets/Target21.jpg': - return const Color(0xFFDA6868); // Muted red - case 'assets/targets/Target22.jpg': - return const Color(0xFFFDFAF3); // Off-white - case 'assets/targets/Target23.jpg': - return const Color(0xFFFFFFFD); // White - case 'assets/targets/Target24.jpg': - return const Color(0xFFFFC4C6); // Light pink - default: - return Colors.white; // Fallback to white - } + return TargetColors.getBackgroundColor(targetPath); } Future _loadBulletColor() async { - final prefs = await SharedPreferences.getInstance(); - final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; + final prefs = await SharedPreferences.getInstance(); + final selectedColor = prefs.getString('bulletColor') ?? 'Blue'; - setState(() { - _bulletColor = _getColorFromName(selectedColor); - }); -} + setState(() { + _bulletColor = _getColorFromName(selectedColor); + }); + } Color _getColorFromName(String colorName) { - switch (colorName.toLowerCase()) { - case 'blue': - return Colors.blue; - case 'green': - return Colors.green; - case 'yellow': - return Colors.yellow; - case 'purple': - return Colors.purple; - case 'cyan': - return Colors.cyan; - case 'teal': - return Colors.teal; - case 'indigo': - return Colors.indigo; - case 'pink': - return Colors.pink; - case 'lime': - return Colors.lime; - case 'mint': - return Colors.greenAccent; - case 'orange': - return Colors.orange; - case 'violet': - return Colors.deepPurpleAccent; - case 'amber': - return Colors.amber; - case 'brown': - return Colors.brown; - case 'grey': - return Colors.grey; - default: - return Colors.blue; // fallback + switch (colorName.toLowerCase()) { + case 'blue': + return Colors.blue; + case 'green': + return Colors.green; + case 'yellow': + return Colors.yellow; + case 'purple': + return Colors.purple; + case 'cyan': + return Colors.cyan; + case 'teal': + return Colors.teal; + case 'indigo': + return Colors.indigo; + case 'pink': + return Colors.pink; + case 'lime': + return Colors.lime; + case 'mint': + return Colors.greenAccent; + case 'orange': + return Colors.orange; + case 'violet': + return Colors.deepPurpleAccent; + case 'amber': + return Colors.amber; + case 'brown': + return Colors.brown; + case 'grey': + return Colors.grey; + default: + return Colors.blue; // fallback + } } -} - - Future _loadJsonAndImage() async { try { @@ -217,7 +161,8 @@ class _TargetViewScreenState extends State { return; } - _targetImageBytes = Uint8List.fromList(img.encodePng(_targetImageDecoded!)); + _targetImageBytes = + Uint8List.fromList(img.encodePng(_targetImageDecoded!)); final currentFile = widget.jsonFiles[_currentIndex]; final jsonString = await currentFile.readAsString(); @@ -322,7 +267,8 @@ class _TargetViewScreenState extends State { builder: (BuildContext context) { return Dialog( backgroundColor: Colors.grey[900], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( @@ -341,7 +287,8 @@ class _TargetViewScreenState extends State { const SizedBox(height: 16), Text( _t("Are you sure you want to delete this target?"), - style: const TextStyle(color: Colors.white70, fontSize: 16), + style: const TextStyle( + color: Colors.white70, fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 24), @@ -357,7 +304,8 @@ class _TargetViewScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), + padding: + const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -375,7 +323,8 @@ class _TargetViewScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[800], foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), + padding: + const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -389,7 +338,6 @@ class _TargetViewScreenState extends State { ); }, ); - }, ), ], @@ -425,6 +373,10 @@ class _TargetViewScreenState extends State { final scaleX = displayWidth / imgWidth; final scaleY = displayHeight / imgHeight; + // Scale bullet size by target scale factors and display scale factors + final dynamicBulletSize = + (((_scaleFactorX + _scaleFactorY) / 2) * 53) * + ((scaleX + scaleY) / 2); return Stack( children: [ @@ -433,39 +385,42 @@ class _TargetViewScreenState extends State { top: offsetY, width: displayWidth, height: displayHeight, - child: Image.memory(_targetImageBytes!, fit: BoxFit.fill), + child: Image.memory(_targetImageBytes!, + fit: BoxFit.fill), ), ..._scaledBullets.asMap().entries.map((entry) { - final i = entry.key; - final scaledBullet = entry.value; - // Scaled bullets are already scaled to display target dimensions - final dx = scaledBullet.dx * scaleX + offsetX; - final dy = scaledBullet.dy * scaleY + offsetY; - // Score is calculated using original bullet position and default target center - final originalBullet = _bullets[i]; - final score = _scoreForBullet(originalBullet, _targetCenter ?? Offset.zero); - final isHighlighted = widget.isCombinedView - ? (_pressedScoreGroup == score) - : (i == _highlightedBulletIndex); + final i = entry.key; + final scaledBullet = entry.value; + // Scaled bullets are already scaled to display target dimensions + final dx = scaledBullet.dx * scaleX + offsetX; + final dy = scaledBullet.dy * scaleY + offsetY; + // Score is calculated using original bullet position and default target center + final originalBullet = _bullets[i]; + final score = _scoreForBullet( + originalBullet, _targetCenter ?? Offset.zero); + final isHighlighted = widget.isCombinedView + ? (_pressedScoreGroup == score) + : (i == _highlightedBulletIndex); - return Positioned( - left: dx - bulletSize / 2, - top: dy - bulletSize / 2, - child: Container( - width: bulletSize, - height: bulletSize, - decoration: BoxDecoration( - color: _bulletColor.withOpacity(0.8), - shape: BoxShape.circle, - border: Border.all( - color: isHighlighted ? Colors.red : Colors.black, - width: isHighlighted ? 3 : 1, + return Positioned( + left: dx - dynamicBulletSize / 2, + top: dy - dynamicBulletSize / 2, + child: Container( + width: dynamicBulletSize, + height: dynamicBulletSize, + decoration: BoxDecoration( + color: _bulletColor.withValues(alpha: 0.8), + shape: BoxShape.circle, + border: Border.all( + color: isHighlighted + ? Colors.red + : Colors.black, + width: isHighlighted ? 3 : 1, + ), ), ), - ), - ); - }) - .toList(), + ); + }).toList(), ], ); }, @@ -474,7 +429,8 @@ class _TargetViewScreenState extends State { const SizedBox(height: 12), Container( color: _backgroundColor, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( children: widget.isCombinedView ? _buildGroupedScoreButtons() @@ -497,7 +453,8 @@ class _TargetViewScreenState extends State { fit: BoxFit.scaleDown, child: Row( children: [ - const Icon(Icons.track_changes, size: 28, color: Colors.white), + const Icon(Icons.track_changes, + size: 28, color: Colors.white), const SizedBox(width: 8), Text( "${_t('Total score')} $_totalScore", @@ -518,55 +475,146 @@ class _TargetViewScreenState extends State { } List _buildGroupedScoreButtons() { - final Map scoreGroups = {}; - for (final bullet in _bullets) { - final score = _scoreForBullet(bullet, _targetCenter!); - scoreGroups[score] = (scoreGroups[score] ?? 0) + 1; - } + final Map scoreGroups = {}; + for (final bullet in _bullets) { + final score = _scoreForBullet(bullet, _targetCenter!); + scoreGroups[score] = (scoreGroups[score] ?? 0) + 1; + } - // Sort scores descending (10 β†’ 0) - final sortedEntries = scoreGroups.entries.toList() - ..sort((a, b) => b.key.compareTo(a.key)); + // Sort scores descending (10 β†’ 0) + final sortedEntries = scoreGroups.entries.toList() + ..sort((a, b) => b.key.compareTo(a.key)); - return sortedEntries.map((entry) { - final score = entry.key; - final count = entry.value; + return sortedEntries.map((entry) { + final score = entry.key; + final count = entry.value; - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: GestureDetector( - onTapDown: (_) => setState(() => _pressedScoreGroup = score), - onTapUp: (_) => setState(() => _pressedScoreGroup = null), - onTapCancel: () => setState(() => _pressedScoreGroup = null), - child: Container( - height: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - offset: Offset(0, 2), - blurRadius: 6, - ), - ], - ), - child: Column( - children: [ - Container( - height: 30, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: GestureDetector( + onTapDown: (_) => setState(() => _pressedScoreGroup = score), + onTapUp: (_) => setState(() => _pressedScoreGroup = null), + onTapCancel: () => setState(() => _pressedScoreGroup = null), + child: Container( + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + offset: Offset(0, 2), + blurRadius: 6, + ), + ], + ), + 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: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + '${count}x', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.black87, + ), + ), ), ), - alignment: Alignment.center, - child: FittedBox( - fit: BoxFit.scaleDown, + Expanded( + child: Container( + decoration: BoxDecoration( + color: _pressedScoreGroup == score + ? Colors.red[400] + : _bulletColor.withValues(alpha: 0.85), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + '$score', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(); + } + + List _buildIndividualScoreButtons() { + final scoredBullets = _bullets.asMap().entries.map((entry) { + final index = entry.key; + final bullet = entry.value; + final score = _scoreForBullet(bullet, _targetCenter ?? Offset.zero); + return {'index': index, 'score': score, 'bullet': bullet}; + }).toList(); + + // Sort descending by score + scoredBullets + .sort((a, b) => (b['score'] as int).compareTo(a['score'] as int)); + + return scoredBullets.map((entry) { + final int index = entry['index'] as int; + final int score = entry['score'] as int; + + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: GestureDetector( + onTapDown: (_) => setState(() => _highlightedBulletIndex = index), + onTapUp: (_) => setState(() => _highlightedBulletIndex = null), + onTapCancel: () => setState(() => _highlightedBulletIndex = null), + child: Container( + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + offset: Offset(0, 2), + blurRadius: 6, + ), + ], + ), + 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( - '${count}x', + '${index + 1}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, @@ -574,21 +622,18 @@ class _TargetViewScreenState extends State { ), ), ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: _pressedScoreGroup == score - ? Colors.red[400] - : _bulletColor.withOpacity(0.85), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), + Expanded( + child: Container( + decoration: BoxDecoration( + color: _highlightedBulletIndex == index + ? Colors.red[400] + : _bulletColor.withValues(alpha: 0.85), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), ), - ), - alignment: Alignment.center, - child: FittedBox( - fit: BoxFit.scaleDown, + alignment: Alignment.center, child: Text( '$score', style: const TextStyle( @@ -599,102 +644,12 @@ class _TargetViewScreenState extends State { ), ), ), - ), - ], + ], + ), ), ), ), - ), - ); - }).toList(); + ); + }).toList(); + } } - - - - List _buildIndividualScoreButtons() { - final scoredBullets = _bullets.asMap().entries.map((entry) { - final index = entry.key; - final bullet = entry.value; - final score = _scoreForBullet(bullet, _targetCenter ?? Offset.zero); - return {'index': index, 'score': score, 'bullet': bullet}; - }).toList(); - - // Sort descending by score - scoredBullets.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int)); - - return scoredBullets.map((entry) { - final int index = entry['index'] as int; - final int score = entry['score'] as int; - - return Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: GestureDetector( - onTapDown: (_) => setState(() => _highlightedBulletIndex = index), - onTapUp: (_) => setState(() => _highlightedBulletIndex = null), - onTapCancel: () => setState(() => _highlightedBulletIndex = null), - child: Container( - height: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - offset: Offset(0, 2), - blurRadius: 6, - ), - ], - ), - 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( - '${index + 1}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: Colors.black87, - ), - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: _highlightedBulletIndex == index - ? Colors.red[400] - : _bulletColor.withOpacity(0.85), - 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, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - }).toList(); -} - -} \ No newline at end of file diff --git a/lib/screens/yolo_detector.dart b/lib/screens/yolo_detector.dart index 9651b42..2427ace 100644 --- a/lib/screens/yolo_detector.dart +++ b/lib/screens/yolo_detector.dart @@ -83,11 +83,21 @@ class YOLODetector { if (score < confidenceThreshold) continue; + // The model outputs coordinates normalized to [0, 1] relative to the input tensor (inputWidth x inputHeight) + // We need to scale back to the original cropped image coordinates + // The cropped image (cropWidth x cropHeight) was resized to (inputWidth x inputHeight) + + final scaledX1 = x1 * croppedResult.cropWidth; + final scaledY1 = y1 * croppedResult.cropHeight; + final scaledX2 = x2 * croppedResult.cropWidth; + final scaledY2 = y2 * croppedResult.cropHeight; + + // Add offset to get coordinates in the original image space results.add(DetectionResult( - x1 * croppedResult.cropWidth + croppedResult.offsetX, - y1 * croppedResult.cropHeight + croppedResult.offsetY, - x2 * croppedResult.cropWidth + croppedResult.offsetX, - y2 * croppedResult.cropHeight + croppedResult.offsetY, + scaledX1 + croppedResult.offsetX, + scaledY1 + croppedResult.offsetY, + scaledX2 + croppedResult.offsetX, + scaledY2 + croppedResult.offsetY, score, classId, )); diff --git a/lib/utils/target_colors.dart b/lib/utils/target_colors.dart index c2014bf..215d10a 100644 --- a/lib/utils/target_colors.dart +++ b/lib/utils/target_colors.dart @@ -19,14 +19,20 @@ class TargetColors { 'assets/targets/Target14.jpg': Color(0xFFFEE05E), // Light yellow 'assets/targets/Target15.jpg': Color(0xFFFFFFFF), // White 'assets/targets/Target16.jpg': Color(0xFF000000), // Black - 'assets/targets/Target17.jpg': Color(0xFF13132B), // Dark blue + 'assets/targets/Target17.jpg': Color(0xFF21304D), // Dark blue 'assets/targets/Target18.jpg': Color(0xFF050608), // Very dark gray - 'assets/targets/Target19.jpg': Color(0xFFC32534), // Dark red - 'assets/targets/Target20.jpg': Color(0xFFFCF3EE), // Light beige + 'assets/targets/Target19.jpg': Color(0xFF09080D), // Dark + 'assets/targets/Target20.jpg': Color(0xFFA9D9EF), // Light blue 'assets/targets/Target21.jpg': Color(0xFFDA6868), // Muted red - 'assets/targets/Target22.jpg': Color(0xFFFDFAF3), // Off-white - 'assets/targets/Target23.jpg': Color(0xFFFFFFFD), // White - 'assets/targets/Target24.jpg': Color(0xFFFFC4C6), // Light pink + 'assets/targets/Target22.jpg': Color(0xFFFBFAF6), // Off-white + 'assets/targets/Target23.jpg': Color(0xFF000000), // Black + 'assets/targets/Target24.jpg': Color(0xFF000000), // Black + 'assets/targets/Target25.jpg': Color(0xFF040308), // Very dark gray + 'assets/targets/Target26.jpg': Color(0xFF161C2C), // Dark blue-gray + 'assets/targets/Target27.jpg': Color(0xFF28A5DF), // Blue + 'assets/targets/Target28.jpg': Color(0xFFFFFFFF), // White + 'assets/targets/Target29.jpg': Color(0xFF180E27), // Dark purple + 'assets/targets/Target30.jpg': Color(0xFFFDFDFB), // Off-white }; static Color getBackgroundColor(String targetPath) { diff --git a/lib/utils/translations.dart b/lib/utils/translations.dart index f0f61fe..f4a01b0 100644 --- a/lib/utils/translations.dart +++ b/lib/utils/translations.dart @@ -18,6 +18,20 @@ class AppTranslations { 'Slovenian': 'Slovenőčina', }, + // Appearance Settings + 'Appearance': { + 'English': 'Appearance', + 'Slovenian': 'Videz', + }, + 'Appearance Settings': { + 'English': 'Appearance Settings', + 'Slovenian': 'Nastavitve videza', + }, + 'Bullet': { + 'English': 'Bullet', + 'Slovenian': 'Zadetek', + }, + // Bullet color 'Select Bullet Color': { 'English': 'Select Bullet Color',