Skip to content

Commit 816d96b

Browse files
committed
Refactor status bar to use animated stat chips for TX, RX, DISC, and Uploaded counts
1 parent 66e2b46 commit 816d96b

File tree

2 files changed

+111
-14
lines changed

2 files changed

+111
-14
lines changed

android/local.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
sdk.dir=/opt/homebrew/share/android-commandlinetools
22
flutter.sdk=/opt/homebrew/share/flutter
3-
flutter.buildMode=release
3+
flutter.buildMode=debug
44
flutter.versionName=1.0.0
5-
flutter.versionCode=1769283486
5+
flutter.versionCode=1

lib/widgets/status_bar.dart

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)