@@ -215,40 +215,40 @@ class _StatusBarState extends State<StatusBar> {
215215 return Row (
216216 mainAxisSize: MainAxisSize .min,
217217 children: [
218- // TX count chip
219- _buildStatChip (
218+ // TX count chip (animated)
219+ _AnimatedStatChip (
220220 icon: Icons .arrow_upward,
221- value: '${ appState .pingStats .txCount }' ,
221+ value: appState.pingStats.txCount,
222222 color: Colors .green,
223223 onTap: () => _showInfoPopup (context, 'tx' ),
224224 ),
225225
226226 const SizedBox (width: 8 ),
227227
228- // RX count chip
229- _buildStatChip (
228+ // RX count chip (animated)
229+ _AnimatedStatChip (
230230 icon: Icons .arrow_downward,
231- value: '${ appState .pingStats .rxCount }' ,
231+ value: appState.pingStats.rxCount,
232232 color: Colors .blue,
233233 onTap: () => _showInfoPopup (context, 'rx' ),
234234 ),
235235
236236 const SizedBox (width: 8 ),
237237
238- // DISC count chip
239- _buildStatChip (
238+ // DISC count chip (animated)
239+ _AnimatedStatChip (
240240 icon: Icons .radar,
241- value: '${ appState .pingStats .discCount }' ,
241+ value: appState.pingStats.discCount,
242242 color: const Color (0xFF7B68EE ), // DISC purple
243243 onTap: () => _showInfoPopup (context, 'disc' ),
244244 ),
245245
246246 const SizedBox (width: 8 ),
247247
248- // Uploaded count chip
249- _buildStatChip (
248+ // Uploaded count chip (animated)
249+ _AnimatedStatChip (
250250 icon: Icons .cloud_done,
251- value: '${ appState .pingStats .successfulUploads }' ,
251+ value: appState.pingStats.successfulUploads,
252252 color: Colors .teal[400 ]! ,
253253 onTap: () => _showInfoPopup (context, 'upload' ),
254254 ),
@@ -290,3 +290,100 @@ class _StatusBarState extends State<StatusBar> {
290290 );
291291 }
292292}
293+
294+ /// Animated stat chip that bounces and highlights when value increments
295+ class _AnimatedStatChip extends StatefulWidget {
296+ final IconData icon;
297+ final int value;
298+ final Color color;
299+ final VoidCallback ? onTap;
300+
301+ const _AnimatedStatChip ({
302+ required this .icon,
303+ required this .value,
304+ required this .color,
305+ this .onTap,
306+ });
307+
308+ @override
309+ State <_AnimatedStatChip > createState () => _AnimatedStatChipState ();
310+ }
311+
312+ class _AnimatedStatChipState extends State <_AnimatedStatChip >
313+ with SingleTickerProviderStateMixin {
314+ late AnimationController _controller;
315+ late Animation <double > _scaleAnimation;
316+ late Animation <double > _highlightAnimation;
317+
318+ @override
319+ void initState () {
320+ super .initState ();
321+ _controller = AnimationController (
322+ duration: const Duration (milliseconds: 300 ),
323+ vsync: this ,
324+ );
325+
326+ _scaleAnimation = TweenSequence <double >([
327+ TweenSequenceItem (tween: Tween (begin: 1.0 , end: 1.15 ), weight: 40 ),
328+ TweenSequenceItem (tween: Tween (begin: 1.15 , end: 1.0 ), weight: 60 ),
329+ ]).animate (CurvedAnimation (parent: _controller, curve: Curves .easeOut));
330+
331+ _highlightAnimation = TweenSequence <double >([
332+ TweenSequenceItem (tween: Tween (begin: 0.15 , end: 0.5 ), weight: 30 ),
333+ TweenSequenceItem (tween: Tween (begin: 0.5 , end: 0.15 ), weight: 70 ),
334+ ]).animate (_controller);
335+ }
336+
337+ @override
338+ void didUpdateWidget (_AnimatedStatChip oldWidget) {
339+ super .didUpdateWidget (oldWidget);
340+ // Trigger animation only on increment
341+ if (widget.value > oldWidget.value) {
342+ _controller.forward (from: 0.0 );
343+ }
344+ }
345+
346+ @override
347+ void dispose () {
348+ _controller.dispose ();
349+ super .dispose ();
350+ }
351+
352+ @override
353+ Widget build (BuildContext context) {
354+ return GestureDetector (
355+ onTap: widget.onTap,
356+ child: AnimatedBuilder (
357+ animation: _controller,
358+ builder: (context, child) {
359+ return Transform .scale (
360+ scale: _scaleAnimation.value,
361+ child: Container (
362+ padding: const EdgeInsets .symmetric (horizontal: 8 , vertical: 4 ),
363+ decoration: BoxDecoration (
364+ color: widget.color.withValues (alpha: _highlightAnimation.value),
365+ borderRadius: BorderRadius .circular (8 ),
366+ border: Border .all (color: widget.color.withValues (alpha: 0.4 )),
367+ ),
368+ child: Row (
369+ mainAxisSize: MainAxisSize .min,
370+ children: [
371+ Icon (widget.icon, size: 14 , color: widget.color),
372+ const SizedBox (width: 4 ),
373+ Text (
374+ '${widget .value }' ,
375+ style: TextStyle (
376+ fontSize: 12 ,
377+ fontWeight: FontWeight .w600,
378+ color: widget.color,
379+ ),
380+ ),
381+ ],
382+ ),
383+ ),
384+ );
385+ },
386+ ),
387+ );
388+ }
389+ }
0 commit comments