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; String _sortMode = 'date'; // or 'score' @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 fallbackDate = folder.statSync().changed; // fallback // Read metadata final metadataFile = File('${folder.path}/metadata.json'); bool isTournament = false; DateTime? createdAt; if (await metadataFile.exists()) { try { final metadata = jsonDecode(await metadataFile.readAsString()); isTournament = metadata['isTournament'] == true; if (metadata['createdAt'] != null) { createdAt = DateTime.tryParse(metadata['createdAt']); } } catch (e) { debugPrint("Error reading metadata for $folderName: $e"); } } // Use createdAt if available, fallback otherwise folderDates[folderName] = createdAt ?? fallbackDate; folderTypes[folderName] = isTournament; // List target files 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); totalScore += _getScoreFromJson(data); } groupedFiles[folderName] = targetFiles; folderScores[folderName] = totalScore; } // Sort folders by stored or fallback created date (DESC) final sortedFolderNames = folderDates.keys.toList() ..sort((a, b) => folderDates[b]!.compareTo(folderDates[a]!)); setState(() { _groupedFiles = { for (var name in sortedFolderNames) name: groupedFiles[name]! }; _folderCreationDates = { for (var name in sortedFolderNames) name: folderDates[name]! }; _folderScores = { for (var name in sortedFolderNames) name: folderScores[name]! }; _folderTypes = { for (var name in sortedFolderNames) name: folderTypes[name]! }; }); } 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() { final List folders = _groupedFiles.keys.toList(); if (_folderFilter != 'all') { final isTournament = _folderFilter == 'tournament'; folders.retainWhere((folderName) => _folderTypes[folderName] == isTournament); } // 🔽 Sort using `createdAt` from _folderCreationDates (which is now based on metadata.json) // 🔽 Apply sorting based on selected sort mode folders.sort((a, b) { if (_sortMode == 'score') { final scoreA = _folderScores[a] ?? 0; final scoreB = _folderScores[b] ?? 0; return scoreB.compareTo(scoreA); // High score first } else { final dateA = _folderCreationDates[a]; final dateB = _folderCreationDates[b]; if (dateA == null || dateB == null) return 0; return dateB.compareTo(dateA); // Newest first } }); return folders; } 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 + tag update) 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); } } // ✅ Preserve existing createdAt value or set it if missing final metadataFile = File('${newFolderPath.path}/metadata.json'); String? createdAt; if (await metadataFile.exists()) { try { final existing = jsonDecode(await metadataFile.readAsString()); createdAt = existing['createdAt']; } catch (_) {} } createdAt ??= DateTime.now().toIso8601String(); // fallback if not found final updatedMetadata = { 'isTournament': isTournament, 'createdAt': createdAt, }; await metadataFile.writeAsString(jsonEncode(updatedMetadata)); _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"), ), ), ], ), ), ); }, ); } Widget _buildFolderRow(String folderName) { final numberOfTargets = _groupedFiles[folderName]!.length; final totalScore = _folderScores[folderName]; final folderDate = _folderCreationDates[folderName]; final formattedDate = folderDate != null ? DateFormat('yyyy-MM-dd').format(folderDate) : 'Unknown Date'; final isExpanded = _expandedFolder == folderName; return Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), decoration: BoxDecoration( color: isExpanded ? _bulletColor.withOpacity(0.15) : Colors.grey[300], borderRadius: BorderRadius.circular(12), border: Border.all( color: isExpanded ? _bulletColor : Colors.transparent, width: 1.5, ), boxShadow: isExpanded ? [ BoxShadow( color: _bulletColor.withOpacity(0.2), blurRadius: 6, offset: const Offset(0, 2), ), ] : [], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( dense: true, // Compact layout contentPadding: EdgeInsets.zero, title: Text( _folderTypes[folderName] == true ? '🏆 $folderName' : folderName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), subtitle: Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( "📝 $numberOfTargets / 🎯 $totalScore / 📅 $formattedDate", style: const TextStyle(fontSize: 13, color: Colors.black87), ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( iconSize: 22, visualDensity: VisualDensity.compact, icon: const Icon(Icons.my_location, color: Colors.grey), onPressed: () => _openFolderTargetView(folderName), ), IconButton( iconSize: 22, visualDensity: VisualDensity.compact, icon: Icon(Icons.edit, color: _bulletColor), onPressed: () => _renameFolder(folderName), ), IconButton( iconSize: 22, visualDensity: VisualDensity.compact, icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _deleteFolder(folderName), ), ], ), onTap: () { setState(() { _expandedFolder = isExpanded ? null : folderName; }); }, ), if (isExpanded) ..._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))) : Scrollbar( thumbVisibility: true, // always show scrollbar thickness: 6, // optional: adjust width radius: const Radius.circular(8), child: 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 StatefulBuilder( builder: (context, setState) { 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: [ const Text( 'Filter Folders', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 24), // Folder type filters _buildStyledFilterButton('all', '📂 All folders', context), const SizedBox(height: 12), _buildStyledFilterButton('tournament', '🏆 Tournament', context), const SizedBox(height: 12), _buildStyledFilterButton('training', '🎯 Training', context), const SizedBox(height: 24), const Text( 'Sort By', style: TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 12), Row( children: [ Expanded( child: GestureDetector( onTap: () { setState(() => _sortMode = 'date'); this.setState(() {}); // 👈 refresh parent widget }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: _sortMode == 'date' ? _bulletColor : Colors.grey[800], borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( '📅 Sort by Date', style: TextStyle( color: _sortMode == 'date' ? Colors.white : Colors.white70, fontWeight: FontWeight.w500, fontSize: 15, ), ), ), ), ), ), Expanded( child: GestureDetector( onTap: () { setState(() => _sortMode = 'score'); this.setState(() {}); // 👈 refresh parent widget }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: _sortMode == 'score' ? _bulletColor : Colors.grey[800], borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( '🏆 Sort by Score', style: TextStyle( color: _sortMode == 'score' ? Colors.white : Colors.white70, fontWeight: FontWeight.w500, fontSize: 15, ), ), ), ), ), ), ], ), ], ), ), ); }, ); }, ); } Widget _buildStyledFilterButton(String value, String label, BuildContext context) { final isSelected = _folderFilter == value; return SizedBox( width: double.infinity, child: GestureDetector( onTap: () { setState(() { _folderFilter = value; }); Navigator.of(context).pop(); }, child: Container( padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: isSelected ? _bulletColor : Colors.grey[800], borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( label, style: TextStyle( color: isSelected ? Colors.white : Colors.white70, fontSize: 16, fontWeight: FontWeight.w500, ), ), ), ), ), ); } }