From 07bc9d81572d804561e0902572063276aefdce9f Mon Sep 17 00:00:00 2001 From: Bl3kiiie Date: Mon, 7 Apr 2025 14:52:23 +0200 Subject: [PATCH] Saving update, bullet update, UI update --- lib/screens/book_screen.dart | 3 +- lib/screens/preview_screen.dart | 286 +++++++++++++++++++++++--------- 2 files changed, 214 insertions(+), 75 deletions(-) diff --git a/lib/screens/book_screen.dart b/lib/screens/book_screen.dart index 57779c7..6326eda 100644 --- a/lib/screens/book_screen.dart +++ b/lib/screens/book_screen.dart @@ -21,6 +21,7 @@ class _BookScreenState extends State with WidgetsBindingObserver { Color _bulletColor = Colors.blue; // default fallback Map _folderTypes = {}; // true = tournament, false = training String _folderFilter = 'all'; + bool isRenameEnabled = false; @override void initState() { @@ -264,7 +265,7 @@ List _filteredFolderNames() { // Rename folder Future _renameFolder(String oldFolderName) async { final newNameController = TextEditingController(); - bool isRenameEnabled = false; + bool isTournament = _folderTypes[oldFolderName] == true; final result = await showDialog>( diff --git a/lib/screens/preview_screen.dart b/lib/screens/preview_screen.dart index 0182786..321b771 100644 --- a/lib/screens/preview_screen.dart +++ b/lib/screens/preview_screen.dart @@ -45,12 +45,13 @@ class _PreviewScreenState extends State { int? _highlightedBulletIndex; final List _manualBulletScores = []; - bool _hasMissingBullets = false; Color _bulletColor = Colors.blue; // default fallback double get _bulletOpacity => 0.8; Color get _bulletColorWithOpacity => _bulletColor.withOpacity(_bulletOpacity); + bool _isEditMode = false; + int? _selectedBulletIndexInEdit; @override @@ -102,17 +103,6 @@ Color _getColorFromName(String colorName) { } } - Future _handleMissingBullets() async { - final currentDetected = _mappedBullets.length; - final result = await _showMismatchOptionsDialog(currentDetected); - if (result == 'retry') { - Navigator.pop(context); // Go back to camera screen - } else if (result == 'manual') { - _manualScoreEntry(); - } -} - - Offset _generatePositionForScore(int score, Offset center) { const double bulletRadius = 25; final List rings = [14, 70, 125, 180, 235, 290, 345, 400, 455, 510]; @@ -213,12 +203,25 @@ Color _getColorFromName(String colorName) { // 10. Update UI final missing = widget.bulletCount - mappedBullets.length; + setState(() { _mappedBullets = mappedBullets; _isLoading = false; - _hasMissingBullets = missing > 0; }); + if (missing > 0) { + final result = await _showMismatchOptionsDialog(mappedBullets.length); + + if (result == 'retry') { + Navigator.pop(context); // Go back to camera screen + } else if (result == 'manual') { + _manualScoreEntry(); // Reuse your original manual dialog + } + } + + + + } catch (e, stack) { debugPrint("❌ Error during detection mapping: $e"); debugPrint(stack.toString()); @@ -439,25 +442,116 @@ void _manualScoreEntry() { } } + Widget _buildEditController() { + if (!_isEditMode || _selectedBulletIndexInEdit == null) return const SizedBox.shrink(); + + return Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.75), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.arrow_drop_up, size: 32, color: Colors.white), + onPressed: () => _moveBullet(const Offset(0, -3)), + splashRadius: 20, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.arrow_left, size: 32, color: Colors.white), + onPressed: () => _moveBullet(const Offset(-3, 0)), + splashRadius: 20, + ), + const SizedBox(width: 4), + IconButton( + icon: const Icon(Icons.check, size: 28, color: Colors.greenAccent), + onPressed: () => setState(() => _selectedBulletIndexInEdit = null), + splashRadius: 20, + ), + const SizedBox(width: 4), + IconButton( + icon: const Icon(Icons.arrow_right, size: 32, color: Colors.white), + onPressed: () => _moveBullet(const Offset(3, 0)), + splashRadius: 20, + ), + ], + ), + IconButton( + icon: const Icon(Icons.arrow_drop_down, size: 32, color: Colors.white), + onPressed: () => _moveBullet(const Offset(0, 3)), + splashRadius: 20, + ), + ], + ), + ), + ), + ); +} + + +void _moveBullet(Offset offsetDelta) { + setState(() { + final index = _selectedBulletIndexInEdit!; + final current = _mappedBullets[index]; + final updated = current + offsetDelta; + + _mappedBullets[index] = updated; + + // If this is a manually added bullet, update its score live + final manualStartIndex = _mappedBullets.length - _manualBulletScores.length; + if (index >= manualStartIndex) { + final updatedScore = _scoreForBullet( + updated, + _targetImageCenterForScoring ?? fixedRingTargetCenter, + ); + _manualBulletScores[index - manualStartIndex] = updatedScore; + } + }); +} + Future _showSaveOptionsDialog() async { - TextEditingController _controller = TextEditingController(); - bool isNewFolderEnabled = false; - bool isTournament = false; // 👈 new flag - final directory = await getApplicationDocumentsDirectory(); final saveDirectory = Directory('${directory.path}/saved_images'); - final List existingFolders = []; + // Ensure directory exists + if (!await saveDirectory.exists()) { + await saveDirectory.create(recursive: true); + } + + // Load existing folders and their modified time + final folderStats = >[]; final folders = saveDirectory.listSync(); for (var folder in folders) { if (folder is Directory) { - existingFolders.add(folder.path.split('/').last); + final stat = folder.statSync(); + folderStats.add({ + 'name': folder.path.split('/').last, + 'changed': stat.changed, + }); } } - String? lastFolderName = existingFolders.isNotEmpty ? existingFolders.last : null; + // Sort by modified time, newest first + folderStats.sort((a, b) => + (b['changed'] as DateTime).compareTo(a['changed'] as DateTime)); + String? lastFolderName = + folderStats.isNotEmpty ? folderStats.first['name'] : null; + + TextEditingController _controller = TextEditingController(); + bool isNewFolderEnabled = folderStats.isEmpty; // pre-enable if none + bool isTournament = false; return await showDialog( context: context, @@ -485,7 +579,6 @@ void _manualScoreEntry() { textAlign: TextAlign.center, ), ), - const Text( "Select where you'd like to save the data:", style: TextStyle(color: Colors.white70), @@ -493,6 +586,7 @@ void _manualScoreEntry() { ), const SizedBox(height: 16), + // Quick save to newest folder if (lastFolderName != null) ...[ SizedBox( width: double.infinity, @@ -503,7 +597,8 @@ void _manualScoreEntry() { style: ElevatedButton.styleFrom( backgroundColor: _bulletColor, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -514,7 +609,7 @@ void _manualScoreEntry() { const SizedBox(height: 16), ], - // New folder name input + // Always show folder name input SizedBox( width: double.infinity, child: TextField( @@ -526,7 +621,8 @@ void _manualScoreEntry() { fillColor: Colors.grey[800], hintText: "Enter a new folder name", hintStyle: const TextStyle(color: Colors.white54), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 14), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -547,7 +643,6 @@ void _manualScoreEntry() { // Tournament toggle if (isNewFolderEnabled) ...[ - const SizedBox(height: 12), GestureDetector( onTap: () { setState(() { @@ -557,19 +652,28 @@ void _manualScoreEntry() { child: Container( width: double.infinity, decoration: BoxDecoration( - color: isTournament ? _bulletColor.withOpacity(0.2) : Colors.grey[850], + color: isTournament + ? _bulletColor.withOpacity(0.2) + : Colors.grey[850], borderRadius: BorderRadius.circular(12), border: Border.all( - color: isTournament ? _bulletColor : Colors.grey[700]!, + color: isTournament + ? _bulletColor + : Colors.grey[700]!, width: 1.5, ), ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 14), child: Center( child: Text( - isTournament ? 'Tournament Mode 🏆' : 'Mark as Tournament 🏆', + isTournament + ? 'Tournament Mode 🏆' + : 'Mark as Tournament 🏆', style: TextStyle( - color: isTournament ? _bulletColor : Colors.white70, + color: isTournament + ? _bulletColor + : Colors.white70, fontWeight: FontWeight.w500, fontSize: 16, ), @@ -577,12 +681,10 @@ void _manualScoreEntry() { ), ), ), + const SizedBox(height: 16), ], - - const SizedBox(height: 16), - - // Create new folder + // Create folder button SizedBox( width: double.infinity, child: ElevatedButton( @@ -596,7 +698,8 @@ void _manualScoreEntry() { ) : null, style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 14), backgroundColor: isNewFolderEnabled ? _bulletColor : Colors.grey[700], foregroundColor: Colors.white, @@ -618,6 +721,7 @@ void _manualScoreEntry() { ); } + Map? _getRingCenterSize(List results) { try { final ring = results.firstWhere((r) => r.classId == 2); @@ -673,7 +777,6 @@ void _manualScoreEntry() { return autoScore + manualScore; } - @override Widget build(BuildContext context) { return Scaffold( @@ -688,7 +791,9 @@ Widget build(BuildContext context) { actions: [ IconButton( icon: Icon( - _mode == PreviewMode.mappedTarget ? Icons.camera_alt : Icons.center_focus_strong, + _mode == PreviewMode.mappedTarget + ? Icons.camera_alt + : Icons.center_focus_strong, ), color: Colors.white, onPressed: () { @@ -809,7 +914,7 @@ Widget build(BuildContext context) { final double bulletSize = ((scaleX + scaleY) / 2) * 55; final isHighlighted = index == _highlightedBulletIndex; - final isManual = index >= (_mappedBullets.length - _manualBulletScores.length); + final isSelectedForEdit = index == _selectedBulletIndexInEdit; return Positioned( left: dx - bulletSize / 2, @@ -818,18 +923,20 @@ Widget build(BuildContext context) { width: bulletSize, height: bulletSize, decoration: BoxDecoration( - color: isManual - ? Colors.red.withOpacity(0.9) - : _bulletColorWithOpacity, + color: _bulletColorWithOpacity, shape: BoxShape.circle, border: Border.all( - color: isHighlighted ? Colors.red : Colors.black, - width: isHighlighted ? 3 : 1, - ), + color: isSelectedForEdit + ? Colors.red + : (!_isEditMode && isHighlighted) + ? Colors.red + : Colors.black, + width: (isSelectedForEdit || (!_isEditMode && isHighlighted)) ? 3 : 1, ), ), - ); - }).toList(), + ), + ); + }).toList(), ], ); }, @@ -851,15 +958,32 @@ Widget build(BuildContext context) { bullet, _targetImageCenterForScoring ?? fixedRingTargetCenter, ); - final isSelected = _highlightedBulletIndex == index; + final isSelected = index == (_isEditMode ? _selectedBulletIndexInEdit : _highlightedBulletIndex); return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: GestureDetector( - onTapDown: (_) => setState(() => _highlightedBulletIndex = index), - onTapUp: (_) => setState(() => _highlightedBulletIndex = null), - onTapCancel: () => setState(() => _highlightedBulletIndex = null), + onTap: () { + if (_isEditMode) { + setState(() => _selectedBulletIndexInEdit = index); + } + }, + onTapDown: (_) { + if (!_isEditMode) { + setState(() => _highlightedBulletIndex = index); + } + }, + onTapUp: (_) { + if (!_isEditMode) { + setState(() => _highlightedBulletIndex = null); + } + }, + onTapCancel: () { + if (!_isEditMode) { + setState(() => _highlightedBulletIndex = null); + } + }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), height: 100, @@ -867,9 +991,10 @@ Widget build(BuildContext context) { borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( - color: Colors.black26, - offset: Offset(0, 2), - blurRadius: 6), + color: Colors.black26, + offset: Offset(0, 2), + blurRadius: 6, + ), ], ), child: Column( @@ -898,9 +1023,7 @@ Widget build(BuildContext context) { decoration: BoxDecoration( color: isSelected ? Colors.red[400] - : (isManual - ? Colors.red[600] - : _bulletColor), + : _bulletColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), @@ -930,24 +1053,39 @@ Widget build(BuildContext context) { const SizedBox(height: 12), ], ), - if (_hasMissingBullets) - Positioned( - top: 12, - right: 12, - child: ElevatedButton.icon( - onPressed: _handleMissingBullets, - icon: const Icon(Icons.warning_amber_rounded, color: Colors.white, size: 20), - label: const Text("Fix Bullets", style: TextStyle(color: Colors.white)), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[600], - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - elevation: 4, - ), - ), - ), + + Positioned( + top: 12, + right: 12, + child: ElevatedButton.icon( + onPressed: () { + setState(() { + _isEditMode = !_isEditMode; + _selectedBulletIndexInEdit = null; + }); + }, + icon: Icon( + _isEditMode ? Icons.check : Icons.edit, + color: Colors.white, + size: 20, + ), + label: Text( + _isEditMode ? "Done" : "Edit Bullets", + style: const TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: _bulletColor, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 4, + ), + ), +), + + + _buildEditController(), ], ), bottomNavigationBar: BottomAppBar(