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'; import '../utils/translations.dart'; import '../utils/target_colors.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 = []; List _scaledBullets = []; // Bullets scaled to display target size int _totalScore = 0; Offset? _targetCenter; Uint8List? _targetImageBytes; img.Image? _targetImageDecoded; String _date = ''; int? _highlightedBulletIndex; int? _pressedScoreGroup; late String _targetFilePath; late String targetAssetPath; late String _selectedTargetPath; // For display double _scaleFactorX = 1.0; // Scale factor for X coordinate double _scaleFactorY = 1.0; // Scale factor for Y coordinate Color _bulletColor = Colors.blue; // default/fallback bullet color String _selectedLanguage = 'English'; Color _backgroundColor = Colors.white; @override void initState() { super.initState(); targetAssetPath = 'assets/target.png'; // Default _initializeScreen(); } Future _initializeScreen() async { await Future.wait([ _loadBulletColor(), _loadLanguage(), _loadSelectedTarget(), ]); await _loadJsonAndImage(); } String _t(String key) { return AppTranslations.translate(key, _selectedLanguage); } Future _loadLanguage() async { final prefs = await SharedPreferences.getInstance(); setState(() { _selectedLanguage = prefs.getString('language') ?? 'English'; }); } Future _loadSelectedTarget() async { final prefs = await SharedPreferences.getInstance(); final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png'; // Always use default target for bullet positioning targetAssetPath = 'assets/target.png'; // Store selected target for display _selectedTargetPath = selectedTarget; _backgroundColor = _getBackgroundColorForTarget(selectedTarget); } Color _getBackgroundColorForTarget(String targetPath) { return TargetColors.getBackgroundColor(targetPath); } 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 { // Load default target for bullet positioning reference final defaultByteData = await rootBundle.load(targetAssetPath); final defaultBytes = defaultByteData.buffer.asUint8List(); img.Image? defaultTargetDecoded = img.decodeImage(defaultBytes); if (defaultTargetDecoded == null) { print("Failed to decode default target image."); return; } // Calculate target center from default target (for bullet positioning) _targetCenter = Offset( defaultTargetDecoded.width / 2, defaultTargetDecoded.height / 2 - 1, ); // Load selected target for display final displayByteData = await rootBundle.load(_selectedTargetPath); final displayBytes = displayByteData.buffer.asUint8List(); _targetImageDecoded = img.decodeImage(displayBytes); if (_targetImageDecoded == null) { print("Failed to decode display target image."); return; } _targetImageBytes = Uint8List.fromList(img.encodePng(_targetImageDecoded!)); final currentFile = widget.jsonFiles[_currentIndex]; final jsonString = await currentFile.readAsString(); final data = jsonDecode(jsonString); // Parse and convert date format from yyyy-m-d to dd/MM/yyyy String rawDate = data['date'] ?? 'Unknown Date'; if (rawDate != 'Unknown Date' && rawDate.contains('-')) { try { final parts = rawDate.split('-'); if (parts.length == 3) { final year = parts[0]; final month = parts[1].padLeft(2, '0'); final day = parts[2].padLeft(2, '0'); _date = '$day/$month/$year'; } else { _date = rawDate; } } catch (e) { _date = rawDate; } } else { _date = rawDate; } final bulletsJson = data['bullets'] as List; _bullets = bulletsJson.map((b) => Offset(b['x'], b['y'])).toList(); _targetFilePath = currentFile.path; // Calculate scale factors based on image dimensions _scaleFactorX = _targetImageDecoded!.width / defaultTargetDecoded.width; _scaleFactorY = _targetImageDecoded!.height / defaultTargetDecoded.height; // Scale bullets to match display target size _scaledBullets = _bullets.map((bullet) { return Offset( bullet.dx * _scaleFactorX, bullet.dy * _scaleFactorY, ); }).toList(); _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: _backgroundColor, appBar: AppBar( title: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.calendar_today, size: 20, color: Colors.white), const SizedBox(width: 8), Text(_date, style: const TextStyle(color: Colors.white)), ], ), backgroundColor: Colors.grey[900], centerTitle: true, iconTheme: const IconThemeData(color: Colors.white), 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: [ Text( _t('Delete Target'), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( _t("Are you sure you want to delete this target?"), style: const 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: Text(_t("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; // Scale bullet size by target scale factors and display scale factors final dynamicBulletSize = (((_scaleFactorX + _scaleFactorY) / 2) * 53) * ((scaleX + scaleY) / 2); return Stack( children: [ Positioned( left: offsetX, top: offsetY, width: displayWidth, height: displayHeight, 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); 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(), ], ); }, ), ), const SizedBox(height: 12), Container( color: _backgroundColor, 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], padding: EdgeInsets.zero, child: SizedBox( height: 150, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Combined text with icon and score (no box) FittedBox( fit: BoxFit.scaleDown, child: Row( children: [ const Icon(Icons.track_changes, size: 28, color: Colors.white), const SizedBox(width: 8), Text( "${_t('Total score')} $_totalScore", style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, ), ), ], ), ), ], ), ), ), ); } 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.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( '${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.withValues(alpha: 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(); } }