From 99da5cf1ca1155308a5a2638e7c9e530b06fc1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCllner?= Date: Wed, 4 Mar 2026 10:47:45 +0100 Subject: [PATCH] feat(scan+sharing): polish rapid scan feedback and invite UX states - Add scanner status bar with real-time state (ready/scanning/saved) - Add success pulse styling after accepted detection for faster repeated scans - Keep scanner in uninterrupted loop while giving clear visual confirmation - Add invite busy state in manage collection to prevent duplicate submits - Improve collaborative management responsiveness with explicit inviting state --- lib/scanner_screen.dart | 48 +++++++++++++++++++++-- lib/screens/manage_collection_screen.dart | 9 ++++- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/scanner_screen.dart b/lib/scanner_screen.dart index a708a28..29b1b7c 100644 --- a/lib/scanner_screen.dart +++ b/lib/scanner_screen.dart @@ -24,6 +24,8 @@ class _ScannerScreenState extends State { bool _isBusy = false; bool _cameraReady = false; String? _lastDetected; + bool _scanAccepted = false; + String _statusText = 'Ready to scan'; // Matches typical Hot Wheels model IDs: 2–5 uppercase letters followed by // 2–4 digits, e.g. JKF21, HCV73, GRX33, FYD83. @@ -67,7 +69,10 @@ class _ScannerScreenState extends State { Future _captureAndScan() async { if (_isBusy || _cameraController == null || !_cameraController!.value.isInitialized) return; - setState(() => _isBusy = true); + setState(() { + _isBusy = true; + _statusText = 'Scanning…'; + }); try { final xFile = await _cameraController!.takePicture(); @@ -118,8 +123,16 @@ class _ScannerScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Scan error: $e'), backgroundColor: Colors.red), ); + setState(() => _statusText = 'Scan failed, try again'); } finally { - if (mounted) setState(() => _isBusy = false); + if (mounted) { + setState(() { + _isBusy = false; + if (_statusText == 'Scanning…') { + _statusText = 'Ready to scan'; + } + }); + } } } @@ -134,7 +147,15 @@ class _ScannerScreenState extends State { if (!mounted) return; if (keepScanning) { - setState(() => _lastDetected = null); + setState(() { + _lastDetected = null; + _scanAccepted = true; + _statusText = 'Saved. Ready for next scan'; + }); + Future.delayed(const Duration(milliseconds: 650), () { + if (!mounted) return; + setState(() => _scanAccepted = false); + }); } else { Navigator.of(context).pop(); } @@ -167,6 +188,22 @@ class _ScannerScreenState extends State { ), body: Column( children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + color: _scanAccepted + ? AppColors.success.withValues(alpha: 0.12) + : Colors.black.withValues(alpha: 0.55), + child: Text( + _statusText, + textAlign: TextAlign.center, + style: TextStyle( + color: _scanAccepted ? AppColors.success : Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), // ── Camera preview ── Expanded( child: _cameraReady @@ -192,7 +229,10 @@ class _ScannerScreenState extends State { height: 100, decoration: BoxDecoration( border: Border.all( - color: AppColors.orange.withValues(alpha: 0.8), + color: (_scanAccepted + ? AppColors.success + : AppColors.orange) + .withValues(alpha: 0.9), width: 2.5, ), borderRadius: BorderRadius.circular(16), diff --git a/lib/screens/manage_collection_screen.dart b/lib/screens/manage_collection_screen.dart index 8a20afb..d43ef4e 100644 --- a/lib/screens/manage_collection_screen.dart +++ b/lib/screens/manage_collection_screen.dart @@ -18,6 +18,7 @@ class _ManageCollectionScreenState extends State { late Collection _collection; List _members = []; bool _isLoading = true; + bool _isInviting = false; @override void initState() { @@ -128,6 +129,7 @@ class _ManageCollectionScreenState extends State { } Future _inviteMember() async { + if (_isInviting) return; final emailCtrl = TextEditingController(); final result = await showDialog( @@ -192,6 +194,7 @@ class _ManageCollectionScreenState extends State { } try { + setState(() => _isInviting = true); await CollectionService.inviteByEmail( collectionId: _collection.id, email: email, @@ -200,6 +203,8 @@ class _ManageCollectionScreenState extends State { _loadMembers(); } catch (e) { showGlobalSnackBar('$e', isError: true); + } finally { + if (mounted) setState(() => _isInviting = false); } } @@ -347,9 +352,9 @@ class _ManageCollectionScreenState extends State { const Spacer(), if (_collection.isOwner) TextButton.icon( - onPressed: _inviteMember, + onPressed: _isInviting ? null : _inviteMember, icon: const Icon(Icons.person_add, size: 18), - label: const Text('Invite'), + label: Text(_isInviting ? 'Inviting…' : 'Invite'), ), ], ),