1132 lines
40 KiB
Dart
1132 lines
40 KiB
Dart
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';
|
|
import '../utils/translations.dart';
|
|
import '../utils/target_colors.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)
|
|
Color _bulletColor = Colors.blue; // default fallback
|
|
Map<String, bool> _folderTypes = {}; // true = tournament, false = training
|
|
String _folderFilter = 'all';
|
|
bool isRenameEnabled = false;
|
|
String _sortMode = 'date'; // or 'score'
|
|
String _selectedLanguage = 'English';
|
|
String? _expandedFolder; // Track which folder is expanded
|
|
late Color _backgroundColor;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this); // Register to listen for lifecycle events
|
|
// Initialize with default target background color
|
|
_backgroundColor = TargetColors.getBackgroundColor('assets/target.png');
|
|
_loadBulletColor();
|
|
_loadLanguage();
|
|
_loadSelectedTargetColor();
|
|
_loadTargetDataFiles(); // Initial load when the screen is first created
|
|
}
|
|
|
|
String _t(String key) {
|
|
return AppTranslations.translate(key, _selectedLanguage);
|
|
}
|
|
|
|
Future<void> _loadLanguage() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
setState(() {
|
|
_selectedLanguage = prefs.getString('language') ?? 'English';
|
|
});
|
|
}
|
|
|
|
Future<void> _loadSelectedTargetColor() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final selectedTarget = prefs.getString('selectedTarget') ?? 'assets/target.png';
|
|
setState(() {
|
|
_backgroundColor = TargetColors.getBackgroundColor(selectedTarget);
|
|
});
|
|
}
|
|
|
|
@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 '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
|
|
}
|
|
}
|
|
|
|
|
|
// 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 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<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);
|
|
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<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,
|
|
_t('Are you sure you want to delete this file?'),
|
|
);
|
|
|
|
if (confirmDeletion == true) {
|
|
try {
|
|
await file.delete();
|
|
_loadTargetDataFiles(); // Refresh after deleting the file
|
|
} catch (e) {
|
|
debugPrint("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] ?? [];
|
|
|
|
// Navigate to view screen with all individual files
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => TargetViewScreen(
|
|
jsonFiles: targetFiles, // Pass all individual files
|
|
refreshFolders: _loadTargetDataFiles,
|
|
isCombinedView: true, // 👈 This signals combined/folder view
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<String> _filteredFolderNames() {
|
|
final List<String> 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<void> _renameFolder(String oldFolderName) async {
|
|
final newNameController = TextEditingController();
|
|
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: [
|
|
Text(
|
|
_t('Folder Settings'),
|
|
style: const 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: _t('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.withValues(alpha: 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: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.emoji_events,
|
|
size: 16,
|
|
color: isTournament ? _bulletColor : Colors.white70,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
isTournament ? _t('Tournament Mode') : _t('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: Text(_t('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: Text(_t('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()) {
|
|
debugPrint('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<void> _deleteFolder(String folderName) async {
|
|
bool? confirmDeletion = await _showConfirmationDialog(
|
|
context,
|
|
'${_t('Are you sure you want to delete the folder')} "$folderName" ${_t('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
|
|
} catch (e) {
|
|
debugPrint("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: [
|
|
Text(
|
|
_t('Confirm Deletion'),
|
|
style: const 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: Text(_t("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: Text(_t("Cancel")),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
Widget _buildFolderRow(String folderName) {
|
|
final numberOfTargets = _groupedFiles[folderName]!.length;
|
|
final totalScore = _folderScores[folderName];
|
|
final folderDate = _folderCreationDates[folderName];
|
|
final formattedDate = folderDate != null
|
|
? DateFormat('dd/MM/yyyy').format(folderDate)
|
|
: 'Unknown Date';
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Header section with grey background like toolbars
|
|
InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
_expandedFolder = _expandedFolder == folderName ? null : folderName;
|
|
});
|
|
},
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(12),
|
|
topRight: Radius.circular(12),
|
|
),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[900],
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(12),
|
|
topRight: Radius.circular(12),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
// White indicator line
|
|
Container(
|
|
width: 4,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Row(
|
|
children: [
|
|
if (_folderTypes[folderName] == true) ...[
|
|
const Icon(Icons.emoji_events, size: 18, color: Colors.white),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Text(
|
|
folderName,
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.calendar_today, size: 14, color: Colors.white),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
formattedDate,
|
|
style: const TextStyle(fontSize: 13, color: Colors.white),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Icon(
|
|
_expandedFolder == folderName ? Icons.expand_less : Icons.expand_more,
|
|
color: Colors.white,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Expandable targets list (in the middle)
|
|
if (_expandedFolder == folderName) ...[
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
),
|
|
child: _buildTargetsList(folderName),
|
|
),
|
|
],
|
|
// Bottom section: Info badge and action buttons
|
|
IntrinsicHeight(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Info badge section (non-clickable, matches button style)
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: const Radius.circular(12),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
// Targets count
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.filter_center_focus, size: 16, color: Colors.black87),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
"$numberOfTargets",
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
|
|
),
|
|
],
|
|
),
|
|
// Score
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.emoji_events, size: 16, color: Colors.black87),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
"$totalScore",
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Preview button section
|
|
InkWell(
|
|
onTap: () => _openFolderTargetView(folderName),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _bulletColor.withValues(alpha: 0.2),
|
|
border: Border(
|
|
left: BorderSide(color: Colors.grey[400]!, width: 1),
|
|
),
|
|
),
|
|
child: Icon(Icons.visibility, color: _bulletColor, size: 20),
|
|
),
|
|
),
|
|
// Edit button section
|
|
InkWell(
|
|
onTap: () => _renameFolder(folderName),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _bulletColor.withValues(alpha: 0.2),
|
|
border: Border(
|
|
left: BorderSide(color: Colors.grey[400]!, width: 1),
|
|
),
|
|
),
|
|
child: Icon(Icons.edit, color: _bulletColor, size: 20),
|
|
),
|
|
),
|
|
// Delete button section
|
|
InkWell(
|
|
onTap: () => _deleteFolder(folderName),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.withValues(alpha: 0.2),
|
|
borderRadius: const BorderRadius.only(
|
|
bottomRight: Radius.circular(12),
|
|
),
|
|
border: Border(
|
|
left: BorderSide(color: Colors.grey[400]!, width: 1),
|
|
),
|
|
),
|
|
child: const Icon(Icons.delete, color: Colors.red, size: 20),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTargetsList(String folderName) {
|
|
final targetFiles = _groupedFiles[folderName] ?? [];
|
|
|
|
if (targetFiles.isEmpty) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Text(
|
|
_t("No targets in this folder"),
|
|
style: const TextStyle(color: Colors.black54, fontSize: 14),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: targetFiles.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final file = entry.value;
|
|
final score = _getScoreFromJson(jsonDecode(file.readAsStringSync()));
|
|
final fileName = "${_t('Target')} ${index + 1}";
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.grey[300]!, width: 1),
|
|
),
|
|
child: IntrinsicHeight(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Target info section - Clickable
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => _openFilePreview(file),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
fileName,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Icon(
|
|
Icons.emoji_events,
|
|
color: Colors.black87,
|
|
size: 14,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
"$score",
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Delete button
|
|
InkWell(
|
|
onTap: () => _deleteFile(file),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.withValues(alpha: 0.1),
|
|
border: Border(
|
|
left: BorderSide(color: Colors.grey[300]!, width: 1),
|
|
),
|
|
borderRadius: const BorderRadius.only(
|
|
topRight: Radius.circular(10),
|
|
bottomRight: Radius.circular(10),
|
|
),
|
|
),
|
|
child: const Icon(
|
|
Icons.delete,
|
|
color: Colors.red,
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _backgroundColor,
|
|
body: Column(
|
|
children: [
|
|
// 🔽 Filter dropdown at the top (styled like tab bar)
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[900],
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.grey[800]!, width: 1),
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
child: GestureDetector(
|
|
onTap: () => _showFolderFilterPopup(context),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
_folderFilter == 'all'
|
|
? Icons.folder
|
|
: _folderFilter == 'tournament'
|
|
? Icons.emoji_events
|
|
: Icons.track_changes,
|
|
size: 16,
|
|
color: Colors.black,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
_folderFilter == 'all'
|
|
? _t('Showing: All folders')
|
|
: _folderFilter == 'tournament'
|
|
? _t('Showing: Tournament folders')
|
|
: _t('Showing: Training folders'),
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black),
|
|
),
|
|
],
|
|
),
|
|
Icon(Icons.filter_list, color: Colors.black87, size: 18),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// 📁 Folder list or empty state
|
|
Expanded(
|
|
child: _groupedFiles.isEmpty
|
|
? Center(child: Text(_t("No saved target data found"), style: const TextStyle(fontSize: 18)))
|
|
: Scrollbar(
|
|
thumbVisibility: true, // always show scrollbar
|
|
thickness: 6, // optional: adjust width
|
|
radius: const Radius.circular(8),
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
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: [
|
|
Text(
|
|
_t('Filter Folders'),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Folder type filters
|
|
_buildStyledFilterButton('all', Icons.folder, _t('All folders'), context),
|
|
const SizedBox(height: 12),
|
|
_buildStyledFilterButton('tournament', Icons.emoji_events, _t('Tournament'), context),
|
|
const SizedBox(height: 12),
|
|
_buildStyledFilterButton('training', Icons.track_changes, _t('Training'), context),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
Text(
|
|
_t('Sort By'),
|
|
style: const TextStyle(color: Colors.white70, fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() => _sortMode = 'date');
|
|
this.setState(() {}); // 👈 refresh parent widget
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
|
margin: const EdgeInsets.only(right: 6),
|
|
decoration: BoxDecoration(
|
|
color: _sortMode == 'date' ? _bulletColor : Colors.grey[800],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.calendar_today,
|
|
size: 14,
|
|
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
_t('Sort by Date'),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: _sortMode == 'date' ? Colors.white : Colors.white70,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 14,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() => _sortMode = 'score');
|
|
this.setState(() {}); // 👈 refresh parent widget
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
|
margin: const EdgeInsets.only(left: 6),
|
|
decoration: BoxDecoration(
|
|
color: _sortMode == 'score' ? _bulletColor : Colors.grey[800],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.emoji_events,
|
|
size: 14,
|
|
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
_t('Sort by Score'),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: _sortMode == 'score' ? Colors.white : Colors.white70,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 14,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
Widget _buildStyledFilterButton(String value, IconData icon, 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: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 18,
|
|
color: isSelected ? Colors.white : Colors.white70,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.white70,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
}
|