first commit
This commit is contained in:
@@ -0,0 +1,809 @@
|
||||
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<BookScreen> createState() => _BookScreenState();
|
||||
}
|
||||
|
||||
class _BookScreenState extends State<BookScreen> with WidgetsBindingObserver {
|
||||
Map<String, List<File>> _groupedFiles = {}; // Files grouped by folder
|
||||
Map<String, DateTime> _folderCreationDates = {}; // Folder creation dates
|
||||
Map<String, int> _folderScores = {}; // Folder scores (sum of scores of all target files inside)
|
||||
String? _expandedFolder;
|
||||
Color _bulletColor = Colors.blue; // default fallback
|
||||
Map<String, bool> _folderTypes = {}; // true = tournament, false = training
|
||||
String _folderFilter = 'all';
|
||||
|
||||
@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<void> _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<FileSystemEntity> folders = dataDirectory.listSync().whereType<Directory>().toList();
|
||||
|
||||
Map<String, List<File>> groupedFiles = {};
|
||||
Map<String, DateTime> folderDates = {};
|
||||
Map<String, int> folderScores = {};
|
||||
Map<String, bool> 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<File> targetFiles = Directory(folder.path)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.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<String, dynamic> 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<void> _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<Map<String, dynamic>> 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<String> _filteredFolderNames() {
|
||||
if (_folderFilter == 'all') return _groupedFiles.keys.toList();
|
||||
|
||||
final isTournamentFilter = _folderFilter == 'tournament';
|
||||
|
||||
return _groupedFiles.keys
|
||||
.where((folderName) => _folderTypes[folderName] == isTournamentFilter)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Rename folder
|
||||
Future<void> _renameFolder(String oldFolderName) async {
|
||||
final newNameController = TextEditingController();
|
||||
bool isRenameEnabled = false;
|
||||
bool isTournament = _folderTypes[oldFolderName] == true;
|
||||
|
||||
final result = await showDialog<Map<String, dynamic>>(
|
||||
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<void> _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<bool?> _showConfirmationDialog(BuildContext context, String message) async {
|
||||
return showDialog<bool>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user