import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'dart:convert'; import 'package:image/image.dart' as img; import 'package:shared_preferences/shared_preferences.dart'; class TargetViewScreen extends StatefulWidget { final List jsonFiles; final Function refreshFolders; final bool isCombinedView; const TargetViewScreen({ super.key, required this.jsonFiles, required this.refreshFolders, this.isCombinedView = false, }); @override State createState() => _TargetViewScreenState(); } class _TargetViewScreenState extends State { int _currentIndex = 0; List _bullets = []; int _totalScore = 0; Offset? _targetCenter; Uint8List? _targetImageBytes; img.Image? _targetImageDecoded; String _date = ''; int? _highlightedBulletIndex; int? _pressedScoreGroup; late String _targetFilePath; final String targetAssetPath = 'assets/target.png'; final double bulletSize = 19; Color _bulletColor = Colors.blue; // default/fallback bullet color @override void initState() { super.initState(); _loadBulletColor(); _loadJsonAndImage(); } 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 '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 { final byteData = await rootBundle.load(targetAssetPath); final bytes = byteData.buffer.asUint8List(); _targetImageDecoded = img.decodeImage(bytes); if (_targetImageDecoded == null) { print("Failed to decode image."); return; } _targetImageBytes = Uint8List.fromList(img.encodePng(_targetImageDecoded!)); _targetCenter = Offset( _targetImageDecoded!.width / 2, _targetImageDecoded!.height / 2 - 1, ); final currentFile = widget.jsonFiles[_currentIndex]; final jsonString = await currentFile.readAsString(); final data = jsonDecode(jsonString); _date = data['date'] ?? 'Unknown Date'; final bulletsJson = data['bullets'] as List; _bullets = bulletsJson.map((b) => Offset(b['x'], b['y'])).toList(); _targetFilePath = currentFile.path; _totalScore = _bullets .map((b) => _scoreForBullet(b, _targetCenter!)) .fold(0, (sum, score) => sum + score); setState(() {}); } catch (e) { print("Error loading JSON or image: $e"); } } int _scoreForBullet(Offset bullet, Offset center) { const bulletRadius = 25; final distance = (bullet - center).distance - bulletRadius; final rings = [14, 70, 125, 180, 235, 290, 345, 400, 455, 510]; for (int i = 0; i < rings.length; i++) { if (distance <= rings[i]) return 10 - i; } return 0; } Future _deleteTarget() async { try { final file = File(_targetFilePath); await file.delete(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Target deleted')), ); widget.refreshFolders(); Navigator.pop(context); } catch (e) { print("Error deleting target: $e"); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to delete target')), ); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text('📅 $_date', style: const TextStyle(color: Colors.white)), backgroundColor: Colors.grey[900], centerTitle: true, iconTheme: const IconThemeData(color: Colors.white), actions: [ IconButton( icon: const Icon(Icons.delete), color: Colors.red, onPressed: () { showDialog( context: context, builder: (BuildContext 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, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( 'Delete Target', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), const Text( "Are you sure you want to delete this target?", style: TextStyle(color: Colors.white70, fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 24), // "Yes" Button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.of(context).pop(); _deleteTarget(); // call your delete function }, style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text("Yes, delete"), ), ), const SizedBox(height: 12), // "Cancel" Button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[800], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text("Cancel"), ), ), ], ), ), ); }, ); }, ), ], ), body: _targetImageDecoded == null ? const Center(child: CircularProgressIndicator()) : Column( children: [ Expanded( child: LayoutBuilder( builder: (context, constraints) { final imgWidth = _targetImageDecoded!.width.toDouble(); final imgHeight = _targetImageDecoded!.height.toDouble(); final screenWidth = constraints.maxWidth; final screenHeight = constraints.maxHeight; 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; return Stack( children: [ Positioned( left: offsetX, top: offsetY, width: displayWidth, height: displayHeight, child: Image.memory(_targetImageBytes!, fit: BoxFit.fill), ), ..._bullets.asMap().entries.map((entry) { final i = entry.key; final bullet = entry.value; final dx = bullet.dx * scaleX + offsetX; final dy = bullet.dy * scaleY + offsetY; final score = _scoreForBullet(bullet, _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, ), ), ), ); }) .toList(), ], ); }, ), ), const SizedBox(height: 12), Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( children: widget.isCombinedView ? _buildGroupedScoreButtons() : _buildIndividualScoreButtons(), ), ), const SizedBox(height: 12), ], ), bottomNavigationBar: BottomAppBar( color: Colors.grey[900], child: SizedBox( height: 56, child: Center( child: Text( "🎯 Total Score: $_totalScore", style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ), ); } List _buildGroupedScoreButtons() { 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)); 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), ), ), alignment: Alignment.center, child: FittedBox( fit: BoxFit.scaleDown, child: Text( '${count}x', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.black87, ), ), ), ), 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), ), ), 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( '${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(); } }