import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; // For DateFormat import 'package:path_provider/path_provider.dart'; // For getApplicationDocumentsDirectory import 'target_view.dart'; // Import the TargetViewScreen import 'package:shared_preferences/shared_preferences.dart'; class BookScreen extends StatefulWidget { const BookScreen({super.key}); @override State createState() => _BookScreenState(); } class _BookScreenState extends State with WidgetsBindingObserver { Map> _groupedFiles = {}; // Files grouped by folder Map _folderCreationDates = {}; // Folder creation dates Map _folderScores = {}; // Folder scores (sum of scores of all target files inside) String? _expandedFolder; Color _bulletColor = Colors.blue; // default fallback Map _folderTypes = {}; // true = tournament, false = training String _folderFilter = 'all'; bool isRenameEnabled = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // Register to listen for lifecycle events _loadBulletColor(); _loadTargetDataFiles(); // Initial load when the screen is first created } @override void dispose() { WidgetsBinding.instance.removeObserver(this); // Unregister from lifecycle events when disposed super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { // Reload data when the screen is resumed or comes into view _loadTargetDataFiles(); } } Future _loadBulletColor() async { final prefs = await SharedPreferences.getInstance(); final selected = prefs.getString('bulletColor') ?? 'Blue'; setState(() { _bulletColor = _getColorFromName(selected); }); } Color _getColorFromName(String colorName) { switch (colorName.toLowerCase()) { case 'blue': return Colors.blue; case 'green': return Colors.green; case 'yellow': return Colors.yellow; case 'purple': return Colors.purple; case 'cyan': return Colors.cyan; case 'teal': return Colors.teal; case 'indigo': return Colors.indigo; case 'pink': return Colors.pink; case 'lime': return Colors.lime; case 'mint': return Colors.greenAccent; case 'sky': return Colors.orange; case 'violet': return Colors.deepPurpleAccent; default: return Colors.blue; // fallback } } // Load the target data files from the directory void _loadTargetDataFiles() async { try { final directory = await getApplicationDocumentsDirectory(); final dataDirectory = Directory('${directory.path}/saved_images'); if (!await dataDirectory.exists()) { await dataDirectory.create(recursive: true); } List folders = dataDirectory.listSync().whereType().toList(); Map> groupedFiles = {}; Map folderDates = {}; Map folderScores = {}; Map folderTypes = {}; // ✅ for (var folder in folders) { String folderName = folder.path.split('/').last; DateTime folderDate = folder.statSync().modified; // ✅ Read isTournament flag bool isTournament = false; final metadataFile = File('${folder.path}/metadata.json'); if (await metadataFile.exists()) { try { final metadata = jsonDecode(await metadataFile.readAsString()); isTournament = metadata['isTournament'] == true; } catch (e) { debugPrint("Error reading metadata for $folderName: $e"); } } folderTypes[folderName] = isTournament; List targetFiles = Directory(folder.path) .listSync() .whereType() .where((file) => file.path.endsWith(".json") && !file.path.endsWith("metadata.json")) .toList(); int totalScore = 0; for (var file in targetFiles) { final jsonString = await file.readAsString(); final data = jsonDecode(jsonString); final score = _getScoreFromJson(data); totalScore += score; } groupedFiles[folderName] = targetFiles; folderDates[folderName] = folderDate; folderScores[folderName] = totalScore; } final sortedFolderNames = folderDates.keys.toList() ..sort((a, b) => folderDates[b]!.compareTo(folderDates[a]!)); final sortedGroupedFiles = { for (var folderName in sortedFolderNames) folderName: groupedFiles[folderName]! }; final sortedFolderScores = { for (var folderName in sortedFolderNames) folderName: folderScores[folderName]! }; final sortedFolderDates = { for (var folderName in sortedFolderNames) folderName: folderDates[folderName]! }; final sortedFolderTypes = { for (var folderName in sortedFolderNames) folderName: folderTypes[folderName]! }; setState(() { _groupedFiles = sortedGroupedFiles; _folderCreationDates = sortedFolderDates; _folderScores = sortedFolderScores; _folderTypes = sortedFolderTypes; // ✅ FIXED }); } catch (e) { debugPrint("Error loading target data files: $e"); } } // Function to safely get the score from the JSON data int _getScoreFromJson(Map data) { if (data.containsKey('totalScore') && data['totalScore'] is int) { return data['totalScore']; } return 0; // Default to 0 if the score is not present or invalid } // Delete a file Future _deleteFile(File file) async { bool? confirmDeletion = await _showConfirmationDialog( context, 'Are you sure you want to delete this file?', ); if (confirmDeletion == true) { try { await file.delete(); _loadTargetDataFiles(); // Refresh after deleting the file } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Error deleting file: $e"))); } } } // Open file preview in the TargetViewScreen void _openFilePreview(File targetFile) { Navigator.push( context, MaterialPageRoute( builder: (context) => TargetViewScreen( jsonFiles: [targetFile], // Pass the selected file in a list refreshFolders: _loadTargetDataFiles, // Pass the refreshFolders callback ), ), ); } // Open folder's combined target view in the TargetViewScreen void _openFolderTargetView(String folderName) async { final targetFiles = _groupedFiles[folderName] ?? []; List> allBullets = []; String latestDate = 'Unknown'; for (var file in targetFiles) { final jsonString = await file.readAsString(); final data = jsonDecode(jsonString); final bullets = (data['bullets'] as List) .map((b) => {'x': b['x'], 'y': b['y']}) .toList(); allBullets.addAll(bullets); if (data['date'] != null) { latestDate = data['date']; // You could sort if you want the most recent } } // Create combined JSON structure final combinedJson = jsonEncode({ 'date': latestDate, 'bullets': allBullets, }); // Save this JSON temporarily to disk final tempDir = await getTemporaryDirectory(); final combinedFile = File('${tempDir.path}/combined_${folderName}.json'); await combinedFile.writeAsString(combinedJson); // Navigate to view screen with the combined file Navigator.push( context, MaterialPageRoute( builder: (context) => TargetViewScreen( jsonFiles: [combinedFile], // treat it like a normal file refreshFolders: _loadTargetDataFiles, isCombinedView: true, // 👈 This line ), ), ); } List _filteredFolderNames() { if (_folderFilter == 'all') return _groupedFiles.keys.toList(); final isTournamentFilter = _folderFilter == 'tournament'; return _groupedFiles.keys .where((folderName) => _folderTypes[folderName] == isTournamentFilter) .toList(); } // Rename folder Future _renameFolder(String oldFolderName) async { final newNameController = TextEditingController(); bool isTournament = _folderTypes[oldFolderName] == true; final result = 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 Text( 'Folder Settings', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), TextField( controller: newNameController, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), decoration: InputDecoration( filled: true, fillColor: Colors.grey[800], hintText: 'Enter 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(() { isRenameEnabled = value.trim().isNotEmpty; }); }, ), const SizedBox(height: 16), 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: 20), Row( children: [ Expanded( child: ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[800], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text('Cancel'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () => Navigator.of(context).pop({ 'name': newNameController.text.trim(), 'isTournament': isTournament, }), style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text('Apply'), ), ), ], ) ], ); }, ), ), ); }, ); // ✅ Handle result (rename + update tag) if (result != null) { final newName = result['name']; final isTournament = result['isTournament'] ?? false; final directory = await getApplicationDocumentsDirectory(); final oldFolderPath = Directory('${directory.path}/saved_images/$oldFolderName'); final targetFolderName = (newName != null && newName.isNotEmpty) ? newName : oldFolderName; final newFolderPath = Directory('${directory.path}/saved_images/$targetFolderName'); if (targetFolderName != oldFolderName) { if (await newFolderPath.exists()) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('A folder named "$targetFolderName" already exists.')), ); return; } else { await oldFolderPath.rename(newFolderPath.path); } } final metadataFile = File('${newFolderPath.path}/metadata.json'); if (isTournament) { await metadataFile.writeAsString(jsonEncode({'isTournament': true})); } else { if (await metadataFile.exists()) { await metadataFile.delete(); } } _loadTargetDataFiles(); // refresh UI } } // Delete a folder Future _deleteFolder(String folderName) async { bool? confirmDeletion = await _showConfirmationDialog( context, 'Are you sure you want to delete the folder "$folderName" and all of its contents?', ); if (confirmDeletion == true) { try { final directory = await getApplicationDocumentsDirectory(); final folderPath = Directory('${directory.path}/saved_images/$folderName'); if (await folderPath.exists()) { await folderPath.delete(recursive: true); } _loadTargetDataFiles(); // Refresh file/folder list if (_expandedFolder == folderName) { setState(() { _expandedFolder = null; }); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Folder "$folderName" deleted successfully.'), ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error deleting folder: $e")), ); } } } Future _showConfirmationDialog(BuildContext context, String message) async { return 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( 'Confirm Deletion', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( message, 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(true), 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), // "No" Button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(context).pop(false), 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"), ), ), ], ), ), ); }, ); } // Build the folder row with folder name, trash, and rename options // Build the folder row with folder name, trash, rename options, and view all targets icon Widget _buildFolderRow(String folderName) { final numberOfTargets = _groupedFiles[folderName]!.length; final totalScore = _folderScores[folderName]; return Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( color: Colors.grey[300], // Lighter background for folder borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( contentPadding: const EdgeInsets.all(0), title: Row( children: [ Text(folderName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), if (_folderTypes[folderName] == true) const Padding( padding: EdgeInsets.only(left: 6), child: Text('🏆', style: TextStyle(fontSize: 18)), ), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), // Add padding between title and info child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(Icons.insert_drive_file, color: Colors.black45), const SizedBox(width: 4), // Small space between the icon and the text Text( "$numberOfTargets / 🎯 $totalScore", style: const TextStyle(fontSize: 16, color: Colors.black), ), ], ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.my_location, color: Colors.grey), onPressed: () => _openFolderTargetView(folderName), ), IconButton( icon: Icon(Icons.edit, color: _bulletColor), onPressed: () => _renameFolder(folderName), ), IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _deleteFolder(folderName), ), ], ), onTap: () { setState(() { // If the folder is already expanded, collapse it, otherwise expand it _expandedFolder = _expandedFolder == folderName ? null : folderName; }); }, ), if (_expandedFolder == folderName) // When expanded, display the file row inside the folder ..._groupedFiles[folderName]! .asMap() .entries .map((entry) => _buildFileRow(entry.value, folderName, entry.key)) .toList(), ], ), ); } // Build the row for files in the folder with everything in a single row Widget _buildFileRow(File file, String folderName, int index) { final fileName = "${index + 1} Target"; // Use number starting from 1 final folderDate = _folderCreationDates[folderName]; final formattedDate = folderDate != null ? DateFormat('yyyy-MM-dd').format(folderDate) : 'Unknown Date'; // or any default fallback string final score = _getScoreFromJson(jsonDecode(file.readAsStringSync())); // Read the score for the file return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // Reduced vertical padding for compact height margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), // Tight margin decoration: BoxDecoration( color: Colors.grey[200], // Light grey background for files borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!, width: 1), // Light border around the file ), child: InkWell( onTap: () => _openFilePreview(file), // Open the file preview when the row is tapped child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // Distribute space across the row children: [ // File name Text(fileName, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), // Date Text(formattedDate, style: const TextStyle(fontSize: 12, color: Colors.black54)), // Target emoji and score Text("🎯 $score", style: const TextStyle(fontSize: 14, color: Colors.black54)), // Delete button IconButton( icon: const Icon(Icons.delete, color: Color.fromARGB(255, 252, 102, 92)), onPressed: () => _deleteFile(file), // Trigger file deletion ), ], ), ), ); } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ // 🔽 Filter dropdown at the top Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), child: GestureDetector( onTap: () => _showFolderFilterPopup(context), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.grey[300], // Same as folder background borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _folderFilter == 'all' ? 'Showing: All folders' : _folderFilter == 'tournament' ? 'Showing: 🏆 Tournament folders' : 'Showing: 🎯 Training folders', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const Icon(Icons.filter_list), ], ), ), ), ), // 📁 Folder list or empty state Expanded( child: _groupedFiles.isEmpty ? const Center(child: Text("No saved target data found", style: TextStyle(fontSize: 18))) : ListView.builder( itemCount: _filteredFolderNames().length, itemBuilder: (context, index) { String folderName = _filteredFolderNames()[index]; return _buildFolderRow(folderName); }, ), ), ], ), ); } void _showFolderFilterPopup(BuildContext context) { showDialog( context: context, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: double.infinity, padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( 'Filter Folders', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 24), _buildFilterOption('all', '📂 All folders'), const SizedBox(height: 12), _buildFilterOption('tournament', '🏆 Tournament'), const SizedBox(height: 12), _buildFilterOption('training', '🎯 Training'), ], ), ), ); }, ); } Widget _buildFilterOption(String value, String label) { final isSelected = _folderFilter == value; return GestureDetector( onTap: () { setState(() { _folderFilter = value; }); Navigator.of(context).pop(); }, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: isSelected ? _bulletColor.withOpacity(0.3) : Colors.grey[800], borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? _bulletColor : Colors.transparent, width: 1.5, ), ), child: Center( child: Text( label, style: const TextStyle(color: Colors.white, fontSize: 16), ), ), ), ); } }