import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; 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:math'; import 'package:shared_preferences/shared_preferences.dart'; 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}); @override State createState() => _PreviewScreenState(); } enum PreviewMode { cameraWithDetections, mappedTarget } class _PreviewScreenState extends State { bool _isLoading = true; PreviewMode _mode = PreviewMode.mappedTarget; List _mappedBullets = []; late List _realImageDetections; img.Image? _realImageDecoded; img.Image? _targetImageDecoded; Future? _targetImageFuture; Offset? _targetImageCenterForScoring; Uint8List? _cachedTargetImage; Uint8List? _cachedRealImage; final String targetAssetPath = 'assets/target.png'; final Offset fixedRingTargetCenter = const Offset(602.51, 600.43); final Size fixedRingTargetSize = const Size(355.94, 359.27); int? _highlightedBulletIndex; final List _manualBulletScores = []; Color _bulletColor = Colors.blue; // default fallback double get _bulletOpacity => 0.8; Color get _bulletColorWithOpacity => _bulletColor.withOpacity(_bulletOpacity); bool _isEditMode = false; int? _selectedBulletIndexInEdit; @override void initState() { super.initState(); _realImageDetections = widget.detections; _targetImageFuture = _loadAndDecodeTargetImage(); _loadBulletColor(); // 👈 Add this _runDetectionAndMapping(); } Future _loadBulletColor() async { 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 'sky': return Colors.orange; case 'violet': return Colors.deepPurpleAccent; 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]; final rng = score.clamp(0, 10); final double distance = rng == 0 ? rings.last + bulletRadius + 5 : rings[10 - rng]; final angle = (360 / widget.bulletCount) * (_manualBulletScores.length + 1); 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); final bytes = byteData.buffer.asUint8List(); final tempDir = await getTemporaryDirectory(); final targetFile = File('${tempDir.path}/target.png'); await targetFile.writeAsBytes(bytes); final decoded = img.decodeImage(bytes); if (decoded != null) { _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 UI final missing = widget.bulletCount - mappedBullets.length; setState(() { _mappedBullets = mappedBullets; _isLoading = false; }); if (missing > 0) { final result = await _showMismatchOptionsDialog(mappedBullets.length); if (result == 'retry') { Navigator.pop(context); // Go back to camera screen } else if (result == 'manual') { _manualScoreEntry(); // Reuse your original manual dialog } } } catch (e, stack) { debugPrint("❌ Error during detection mapping: $e"); debugPrint(stack.toString()); setState(() => _isLoading = false); } } Future _showMismatchOptionsDialog(int detectedCount) async { return showDialog( context: context, builder: (context) { final missing = widget.bulletCount - detectedCount; return AlertDialog( backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text( 'Bullet Count Mismatch', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), content: Text( 'Detected $detectedCount bullet(s), expected ${widget.bulletCount}.\n' 'You can retry detection or manually add the ${missing.abs()} missing shot(s).', style: const TextStyle(color: Colors.white70), ), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 12), actionsPadding: const EdgeInsets.only(bottom: 16, left: 16, right: 16), actions: [ Row( children: [ Expanded( child: ElevatedButton( onPressed: () => Navigator.pop(context, 'retry'), style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Icon(Icons.restart_alt, color: Colors.white), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: () => Navigator.pop(context, 'manual'), style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Icon(Icons.edit, color: Colors.white), ), ), ], ), ], ); }, ); } 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: [ const Text( 'Enter Missing Bullet Scores', style: 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: '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: Colors.greenAccent[700], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text('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) } 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.year}-${now.month}-${now.day}"; final timeString = "${now.hour}-${now.minute}-${now.second}"; final fileName = "target_${dateString}_$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(); return Positioned( bottom: 20, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.75), borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.arrow_drop_up, size: 32, color: Colors.white), onPressed: () => _moveBullet(const Offset(0, -3)), splashRadius: 20, ), Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.arrow_left, size: 32, color: Colors.white), onPressed: () => _moveBullet(const Offset(-3, 0)), splashRadius: 20, ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.check, size: 28, color: Colors.greenAccent), onPressed: () => setState(() => _selectedBulletIndexInEdit = null), splashRadius: 20, ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.arrow_right, size: 32, color: Colors.white), onPressed: () => _moveBullet(const Offset(3, 0)), splashRadius: 20, ), ], ), IconButton( icon: const Icon(Icons.arrow_drop_down, size: 32, color: Colors.white), onPressed: () => _moveBullet(const Offset(0, 3)), splashRadius: 20, ), ], ), ), ), ); } 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'); // Ensure directory exists if (!await saveDirectory.exists()) { await saveDirectory.create(recursive: true); } // Load existing folders and their modified time final folderStats = >[]; final folders = saveDirectory.listSync(); for (var folder in folders) { if (folder is Directory) { final stat = folder.statSync(); folderStats.add({ 'name': folder.path.split('/').last, 'changed': stat.changed, }); } } // Sort by modified time, newest first folderStats.sort((a, b) => (b['changed'] as DateTime).compareTo(a['changed'] as DateTime)); String? lastFolderName = folderStats.isNotEmpty ? folderStats.first['name'] : null; TextEditingController _controller = TextEditingController(); bool isNewFolderEnabled = folderStats.isEmpty; // pre-enable if none bool isTournament = false; return await 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: StatefulBuilder( builder: (context, setState) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Padding( padding: EdgeInsets.only(bottom: 16.0), child: Text( 'Save Target Data', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), const Text( "Select where you'd like to save the data:", style: TextStyle(color: Colors.white70), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Quick save to newest folder if (lastFolderName != null) ...[ SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.pop(context, lastFolderName); }, style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text("Save to '$lastFolderName'"), ), ), const SizedBox(height: 16), ], // Always show folder name input SizedBox( width: double.infinity, child: TextField( controller: _controller, style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, decoration: InputDecoration( filled: true, fillColor: Colors.grey[800], hintText: "Enter a new folder name", hintStyle: const TextStyle(color: Colors.white54), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: _bulletColor), ), ), onChanged: (value) { setState(() { isNewFolderEnabled = value.trim().isNotEmpty; }); }, ), ), const SizedBox(height: 12), // Tournament toggle if (isNewFolderEnabled) ...[ GestureDetector( onTap: () { setState(() { isTournament = !isTournament; }); }, child: Container( width: double.infinity, decoration: BoxDecoration( color: isTournament ? _bulletColor.withOpacity(0.2) : Colors.grey[850], borderRadius: BorderRadius.circular(12), border: Border.all( color: isTournament ? _bulletColor : Colors.grey[700]!, width: 1.5, ), ), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14), child: Center( child: Text( isTournament ? 'Tournament Mode 🏆' : 'Mark as Tournament 🏆', style: TextStyle( color: isTournament ? _bulletColor : Colors.white70, fontWeight: FontWeight.w500, fontSize: 16, ), ), ), ), ), const SizedBox(height: 16), ], // Create folder button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: isNewFolderEnabled ? () => Navigator.pop( context, jsonEncode({ 'folder': _controller.text.trim(), 'isTournament': isTournament, }), ) : null, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 14), backgroundColor: isNewFolderEnabled ? _bulletColor : Colors.grey[700], foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey[700], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text("Create New Folder"), ), ), ], ); }, ), ), ); }, ); } Map? _getRingCenterSize(List results) { try { final ring = results.firstWhere((r) => r.classId == 2); final cx = (ring.x1 + ring.x2) / 2; final cy = (ring.y1 + ring.y2) / 2; final width = ring.x2 - ring.x1; final height = ring.y2 - ring.y1; return { 'center': Offset(cx, cy), 'size': Size(width, height), }; } catch (_) { return null; } } 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; return Offset(mappedX, mappedY); } int _scoreForBullet(Offset bullet, Offset centerOverride) { const double bulletRadius = 25; final double distanceToEdge = (bullet - centerOverride).distance - bulletRadius; final List rings = [14, 70, 125, 180, 235, 290, 345, 400, 455, 510]; for (int i = 0; i < rings.length; i++) { if (distanceToEdge <= rings[i]) { return 10 - i; } } return 0; } int get _totalScore { final autoScore = _mappedBullets .take(widget.bulletCount - _manualBulletScores.length) .map((b) => _scoreForBullet(b, _targetImageCenterForScoring ?? fixedRingTargetCenter)) .fold(0, (sum, score) => sum + score); final manualScore = _manualBulletScores.fold(0, (a, b) => a + b); return autoScore + manualScore; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.grey[900], iconTheme: const IconThemeData(color: Colors.white), title: const Text( 'Detection Preview', style: TextStyle(color: Colors.white), ), actions: [ IconButton( icon: Icon( _mode == PreviewMode.mappedTarget ? Icons.camera_alt : Icons.center_focus_strong, ), color: Colors.white, onPressed: () { setState(() { _mode = _mode == PreviewMode.mappedTarget ? PreviewMode.cameraWithDetections : PreviewMode.mappedTarget; }); }, tooltip: _mode == PreviewMode.mappedTarget ? "Show Camera Detections" : "Show Mapped Target", ), IconButton( icon: const Icon(Icons.save), color: Colors.white, onPressed: _saveTargetData, tooltip: "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 imgData = _mode == PreviewMode.mappedTarget ? _targetImageDecoded : _realImageDecoded; 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; 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; } final scaleX = displayWidth / imgWidth; final scaleY = displayHeight / imgHeight; 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, ), ), ), ), ), ); }).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; return Positioned( left: dx - bulletSize / 2, top: dy - bulletSize / 2, child: Container( width: bulletSize, height: bulletSize, decoration: BoxDecoration( color: _bulletColorWithOpacity, shape: BoxShape.circle, border: Border.all( color: isSelectedForEdit ? Colors.red : (!_isEditMode && isHighlighted) ? Colors.red : Colors.black, width: (isSelectedForEdit || (!_isEditMode && isHighlighted)) ? 3 : 1, ), ), ), ); }).toList(), ], ); }, ), ), const SizedBox(height: 12), if (_mappedBullets.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), 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, ); final isSelected = index == (_isEditMode ? _selectedBulletIndexInEdit : _highlightedBulletIndex); 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: isSelected ? Colors.red[400] : _bulletColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), ), alignment: Alignment.center, child: Text( '$score', style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ), ), ), ); }, ), ), ), const SizedBox(height: 12), ], ), Positioned( top: 12, right: 12, child: ElevatedButton.icon( onPressed: () { setState(() { _isEditMode = !_isEditMode; _selectedBulletIndexInEdit = null; }); }, icon: Icon( _isEditMode ? Icons.check : Icons.edit, color: Colors.white, size: 20, ), label: Text( _isEditMode ? "Done" : "Edit Bullets", style: const TextStyle(color: Colors.white), ), style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 4, ), ), ), _buildEditController(), ], ), bottomNavigationBar: BottomAppBar( color: Colors.grey[900], child: SizedBox( height: 56, child: Center( child: Text( "🎯 Total Score: $_totalScore", style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ), ); } }