neoagent 2.3.1-beta.54 → 2.3.1-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,83 +59,107 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
59
59
  return ListView(
60
60
  padding: _pagePadding(context),
61
61
  children: <Widget>[
62
- const _SectionTitle('Recordings'),
63
- const SizedBox(height: 12),
62
+ const _PageTitle(
63
+ title: 'Recordings',
64
+ subtitle:
65
+ 'Capture sessions, monitor device state, and review transcripts with clearer status and playback controls.',
66
+ ),
64
67
  Card(
65
68
  child: Padding(
66
69
  padding: const EdgeInsets.all(22),
67
70
  child: Column(
68
71
  crossAxisAlignment: CrossAxisAlignment.start,
69
72
  children: <Widget>[
70
- Wrap(
71
- spacing: 10,
72
- runSpacing: 10,
73
- crossAxisAlignment: WrapCrossAlignment.center,
73
+ Row(
74
+ crossAxisAlignment: CrossAxisAlignment.start,
74
75
  children: <Widget>[
75
- _DotStatus(
76
- label: statusLabel,
77
- color: statusColor,
78
- ),
79
- if (runtime.platformLabel != null &&
80
- runtime.platformLabel!.isNotEmpty)
81
- Text(
82
- runtime.platformLabel!,
83
- style: TextStyle(color: _textSecondary),
76
+ Expanded(
77
+ child: Column(
78
+ crossAxisAlignment: CrossAxisAlignment.start,
79
+ children: <Widget>[
80
+ Text(
81
+ 'Capture controls',
82
+ style: _displayTitleStyle(24),
83
+ ),
84
+ const SizedBox(height: 6),
85
+ Text(
86
+ runtime.platformLabel?.trim().isNotEmpty == true
87
+ ? runtime.platformLabel!
88
+ : 'Browser screen + microphone recorder',
89
+ style: TextStyle(
90
+ color: _textSecondary,
91
+ fontSize: 15,
92
+ height: 1.45,
93
+ ),
94
+ ),
95
+ ],
84
96
  ),
97
+ ),
98
+ const SizedBox(width: 16),
99
+ _StatusPill(label: statusLabel, color: statusColor),
85
100
  ],
86
101
  ),
87
102
  const SizedBox(height: 16),
88
- if (isStarting) ...<Widget>[
89
- const SizedBox(height: 14),
90
- Container(
91
- width: double.infinity,
92
- padding: const EdgeInsets.symmetric(
93
- horizontal: 14,
94
- vertical: 12,
95
- ),
96
- decoration: BoxDecoration(
97
- color: _bgSecondary.withValues(alpha: 0.8),
98
- borderRadius: BorderRadius.circular(16),
99
- border: Border.all(color: _borderLight),
100
- ),
101
- child: Row(
102
- children: <Widget>[
103
- const SizedBox.square(
104
- dimension: 16,
105
- child: CircularProgressIndicator(strokeWidth: 2),
106
- ),
107
- const SizedBox(width: 12),
108
- Expanded(
103
+ AnimatedSwitcher(
104
+ duration: const Duration(milliseconds: 220),
105
+ switchInCurve: Curves.easeOutCubic,
106
+ switchOutCurve: Curves.easeInCubic,
107
+ child: isStarting
108
+ ? _GlassSurface(
109
+ key: const ValueKey<String>('starting'),
110
+ width: double.infinity,
111
+ padding: const EdgeInsets.symmetric(
112
+ horizontal: 14,
113
+ vertical: 12,
114
+ ),
115
+ borderRadius: BorderRadius.circular(16),
116
+ blurSigma: 10,
117
+ fillColor: _bgCard.withValues(alpha: 0.88),
118
+ borderColor: _borderLight,
119
+ child: Row(
120
+ children: <Widget>[
121
+ const SizedBox.square(
122
+ dimension: 16,
123
+ child: CircularProgressIndicator(
124
+ strokeWidth: 2,
125
+ ),
126
+ ),
127
+ const SizedBox(width: 12),
128
+ Expanded(
129
+ child: Text(
130
+ 'Starting recording. This can take a few seconds while the session and permissions are prepared.',
131
+ style: TextStyle(
132
+ color: _textSecondary,
133
+ height: 1.4,
134
+ ),
135
+ ),
136
+ ),
137
+ ],
138
+ ),
139
+ )
140
+ : isStopping
141
+ ? _GlassSurface(
142
+ key: const ValueKey<String>('stopping'),
143
+ width: double.infinity,
144
+ padding: const EdgeInsets.symmetric(
145
+ horizontal: 14,
146
+ vertical: 12,
147
+ ),
148
+ borderRadius: BorderRadius.circular(16),
149
+ blurSigma: 10,
150
+ fillColor: _bgCard.withValues(alpha: 0.88),
151
+ borderColor: _borderLight,
109
152
  child: Text(
110
- 'Starting recording. This can take a few seconds while the session and permissions are prepared.',
153
+ 'Finalizing recording...',
111
154
  style: TextStyle(
112
155
  color: _textSecondary,
113
156
  height: 1.4,
114
157
  ),
115
158
  ),
116
- ),
117
- ],
118
- ),
119
- ),
120
- ] else if (isStopping) ...<Widget>[
121
- const SizedBox(height: 14),
122
- Container(
123
- width: double.infinity,
124
- padding: const EdgeInsets.symmetric(
125
- horizontal: 14,
126
- vertical: 12,
127
- ),
128
- decoration: BoxDecoration(
129
- color: _bgSecondary.withValues(alpha: 0.8),
130
- borderRadius: BorderRadius.circular(16),
131
- border: Border.all(color: _borderLight),
132
- ),
133
- child: Text(
134
- 'Finalizing recording...',
135
- style: TextStyle(color: _textSecondary, height: 1.4),
136
- ),
137
- ),
138
- ],
159
+ )
160
+ : const SizedBox.shrink(),
161
+ ),
162
+ if (isStarting || isStopping) const SizedBox(height: 14),
139
163
  const SizedBox(height: 18),
140
164
  Wrap(
141
165
  spacing: 12,
@@ -240,14 +264,13 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
240
264
  ),
241
265
  if (runtime.supportsSystemAudio) ...<Widget>[
242
266
  const SizedBox(height: 20),
243
- Container(
267
+ _GlassSurface(
244
268
  width: double.infinity,
245
269
  padding: const EdgeInsets.all(18),
246
- decoration: BoxDecoration(
247
- color: _bgSecondary.withValues(alpha: 0.72),
248
- borderRadius: BorderRadius.circular(22),
249
- border: Border.all(color: _borderLight),
250
- ),
270
+ borderRadius: BorderRadius.circular(22),
271
+ blurSigma: 10,
272
+ fillColor: _bgCard.withValues(alpha: 0.9),
273
+ borderColor: _borderLight,
251
274
  child: Column(
252
275
  crossAxisAlignment: CrossAxisAlignment.start,
253
276
  children: <Widget>[
@@ -370,7 +393,6 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
370
393
  ),
371
394
  ),
372
395
  ),
373
- const SizedBox(height: 20),
374
396
  const _SectionTitle('Transcripts'),
375
397
  const SizedBox(height: 12),
376
398
  if (widget.controller.recordingSessions.isEmpty)
@@ -426,296 +448,312 @@ class _RecordingSessionCard extends StatelessWidget {
426
448
  final runtime = controller.recordingRuntime;
427
449
  final isLiveSession = runtime.active && runtime.sessionId == session.id;
428
450
  final canDeleteRecording = onDeleteRecording != null && !isLiveSession;
429
- return Card(
430
- child: Padding(
431
- padding: const EdgeInsets.all(18),
432
- child: Column(
433
- crossAxisAlignment: CrossAxisAlignment.start,
434
- children: <Widget>[
435
- Row(
436
- crossAxisAlignment: CrossAxisAlignment.start,
437
- children: <Widget>[
438
- Expanded(
439
- child: Column(
440
- crossAxisAlignment: CrossAxisAlignment.start,
441
- children: <Widget>[
442
- Text(
443
- session.title,
444
- style: TextStyle(
445
- fontSize: 16,
446
- fontWeight: FontWeight.w700,
451
+ return AnimatedContainer(
452
+ duration: const Duration(milliseconds: 240),
453
+ curve: Curves.easeOutCubic,
454
+ child: Card(
455
+ child: Padding(
456
+ padding: const EdgeInsets.all(18),
457
+ child: Column(
458
+ crossAxisAlignment: CrossAxisAlignment.start,
459
+ children: <Widget>[
460
+ Row(
461
+ crossAxisAlignment: CrossAxisAlignment.start,
462
+ children: <Widget>[
463
+ Expanded(
464
+ child: Column(
465
+ crossAxisAlignment: CrossAxisAlignment.start,
466
+ children: <Widget>[
467
+ Text(
468
+ session.title,
469
+ style: TextStyle(
470
+ fontSize: 16,
471
+ fontWeight: FontWeight.w700,
472
+ ),
447
473
  ),
448
- ),
449
- const SizedBox(height: 6),
450
- Text(
451
- '${session.startedAtLabel} ${session.platformLabel} • ${session.durationLabel}',
452
- style: TextStyle(color: _textSecondary),
453
- ),
454
- ],
474
+ const SizedBox(height: 6),
475
+ Text(
476
+ '${session.startedAtLabel} • ${session.platformLabel} • ${session.durationLabel}',
477
+ style: TextStyle(color: _textSecondary),
478
+ ),
479
+ ],
480
+ ),
455
481
  ),
456
- ),
457
- _StatusPill(
458
- label: session.statusLabel,
459
- color: session.statusColor,
460
- ),
461
- ],
462
- ),
463
- const SizedBox(height: 12),
464
- Wrap(
465
- spacing: 8,
466
- runSpacing: 8,
467
- children: session.sources
468
- .map(
469
- (source) => Container(
470
- padding: const EdgeInsets.symmetric(
471
- horizontal: 10,
472
- vertical: 7,
473
- ),
474
- decoration: BoxDecoration(
475
- color: _bgSecondary,
482
+ _StatusPill(
483
+ label: session.statusLabel,
484
+ color: session.statusColor,
485
+ ),
486
+ ],
487
+ ),
488
+ const SizedBox(height: 14),
489
+ Wrap(
490
+ spacing: 8,
491
+ runSpacing: 8,
492
+ children: session.sources
493
+ .map(
494
+ (source) => _GlassSurface(
495
+ padding: const EdgeInsets.symmetric(
496
+ horizontal: 10,
497
+ vertical: 7,
498
+ ),
476
499
  borderRadius: BorderRadius.circular(999),
477
- border: Border.all(color: _border),
478
- ),
479
- child: Text(
480
- '${source.label} • ${source.durationLabel}',
481
- style: TextStyle(fontSize: 12),
500
+ blurSigma: 8,
501
+ fillColor: _bgCard.withValues(alpha: 0.82),
502
+ borderColor: _border,
503
+ child: Text(
504
+ '${source.label} ${source.durationLabel}',
505
+ style: TextStyle(fontSize: 12),
506
+ ),
482
507
  ),
483
- ),
484
- )
485
- .toList(),
486
- ),
487
- if (session.sources.any(
488
- (source) => source.mediaKind == 'audio',
489
- )) ...<Widget>[
490
- const SizedBox(height: 12),
491
- _RecordingSourceAudioControls(
492
- controller: controller,
493
- session: session,
508
+ )
509
+ .toList(),
494
510
  ),
495
- ],
496
- if (session.lastError != null &&
497
- session.lastError!.trim().isNotEmpty)
498
- Padding(
499
- padding: const EdgeInsets.only(top: 12),
500
- child: Text(
501
- session.lastError!,
502
- style: TextStyle(color: _danger),
511
+ if (session.sources.any(
512
+ (source) => source.mediaKind == 'audio',
513
+ )) ...<Widget>[
514
+ const SizedBox(height: 12),
515
+ _RecordingSourceAudioControls(
516
+ controller: controller,
517
+ session: session,
503
518
  ),
504
- ),
505
- if (session.structuredContent.isNotEmpty) ...<Widget>[
506
- const SizedBox(height: 16),
507
- Container(
508
- padding: const EdgeInsets.all(14),
509
- decoration: BoxDecoration(
510
- color: _bgSecondary,
511
- borderRadius: BorderRadius.circular(12),
512
- border: Border.all(color: _accent.withValues(alpha: 0.3)),
519
+ ],
520
+ if (session.lastError != null &&
521
+ session.lastError!.trim().isNotEmpty)
522
+ Padding(
523
+ padding: const EdgeInsets.only(top: 12),
524
+ child: Text(
525
+ session.lastError!,
526
+ style: TextStyle(color: _danger),
527
+ ),
513
528
  ),
514
- child: Column(
515
- crossAxisAlignment: CrossAxisAlignment.start,
516
- children: <Widget>[
517
- Row(
518
- children: <Widget>[
519
- Icon(Icons.auto_awesome, size: 16, color: _accent),
520
- const SizedBox(width: 8),
529
+ if (session.structuredContent.isNotEmpty) ...<Widget>[
530
+ const SizedBox(height: 16),
531
+ _GlassSurface(
532
+ padding: const EdgeInsets.all(16),
533
+ borderRadius: BorderRadius.circular(18),
534
+ blurSigma: 8,
535
+ fillColor: _accentMuted.withValues(alpha: 0.16),
536
+ borderColor: _accent.withValues(alpha: 0.26),
537
+ child: Column(
538
+ crossAxisAlignment: CrossAxisAlignment.start,
539
+ children: <Widget>[
540
+ Row(
541
+ children: <Widget>[
542
+ Icon(Icons.auto_awesome, size: 16, color: _accent),
543
+ const SizedBox(width: 8),
544
+ Text(
545
+ 'Insights',
546
+ style: TextStyle(
547
+ color: _accent,
548
+ fontWeight: FontWeight.w600,
549
+ ),
550
+ ),
551
+ ],
552
+ ),
553
+ if (session.structuredContent['summary'] !=
554
+ null) ...<Widget>[
555
+ const SizedBox(height: 10),
521
556
  Text(
522
- 'Insights',
557
+ 'Summary',
523
558
  style: TextStyle(
524
- color: _accent,
525
- fontWeight: FontWeight.w600,
559
+ fontWeight: FontWeight.w700,
560
+ fontSize: 13,
561
+ color: _textSecondary,
526
562
  ),
527
563
  ),
528
- ],
529
- ),
530
- if (session.structuredContent['summary'] !=
531
- null) ...<Widget>[
532
- const SizedBox(height: 10),
533
- Text(
534
- 'Summary',
535
- style: TextStyle(
536
- fontWeight: FontWeight.w700,
537
- fontSize: 13,
538
- color: _textSecondary,
564
+ const SizedBox(height: 4),
565
+ Text(
566
+ session.structuredContent['summary'].toString(),
567
+ style: TextStyle(height: 1.55, fontSize: 15),
539
568
  ),
540
- ),
541
- const SizedBox(height: 4),
542
- Text(
543
- session.structuredContent['summary'].toString(),
544
- style: TextStyle(height: 1.45),
545
- ),
546
- ],
547
- if (session.structuredContent['action_items'] != null &&
548
- _getStructuredList(
549
- session,
550
- 'action_items',
551
- ).isNotEmpty) ...<Widget>[
552
- const SizedBox(height: 10),
553
- Text(
554
- 'Action Items',
555
- style: TextStyle(
556
- fontWeight: FontWeight.w700,
557
- fontSize: 13,
558
- color: _textSecondary,
569
+ ],
570
+ if (session.structuredContent['action_items'] != null &&
571
+ _getStructuredList(
572
+ session,
573
+ 'action_items',
574
+ ).isNotEmpty) ...<Widget>[
575
+ const SizedBox(height: 10),
576
+ Text(
577
+ 'Action Items',
578
+ style: TextStyle(
579
+ fontWeight: FontWeight.w700,
580
+ fontSize: 13,
581
+ color: _textSecondary,
582
+ ),
559
583
  ),
560
- ),
561
- const SizedBox(height: 4),
562
- ..._getStructuredList(session, 'action_items').map(
563
- (item) => Padding(
564
- padding: const EdgeInsets.only(bottom: 4),
565
- child: Row(
566
- crossAxisAlignment: CrossAxisAlignment.start,
567
- children: [
568
- Text(
569
- '• ',
570
- style: TextStyle(
571
- fontWeight: FontWeight.w700,
572
- color: _accent,
584
+ const SizedBox(height: 4),
585
+ ..._getStructuredList(session, 'action_items').map(
586
+ (item) => Padding(
587
+ padding: const EdgeInsets.only(bottom: 4),
588
+ child: Row(
589
+ crossAxisAlignment: CrossAxisAlignment.start,
590
+ children: [
591
+ Text(
592
+ '• ',
593
+ style: TextStyle(
594
+ fontWeight: FontWeight.w700,
595
+ color: _accent,
596
+ ),
573
597
  ),
574
- ),
575
- Expanded(
576
- child: Text(
577
- item.toString(),
578
- style: TextStyle(height: 1.35),
598
+ Expanded(
599
+ child: Text(
600
+ item.toString(),
601
+ style: TextStyle(height: 1.35),
602
+ ),
579
603
  ),
580
- ),
581
- ],
604
+ ],
605
+ ),
582
606
  ),
583
607
  ),
584
- ),
585
- ],
586
- if (session.structuredContent['events'] != null &&
587
- _getStructuredList(
588
- session,
589
- 'events',
590
- ).isNotEmpty) ...<Widget>[
591
- const SizedBox(height: 10),
592
- Text(
593
- 'Events Mentioned',
594
- style: TextStyle(
595
- fontWeight: FontWeight.w700,
596
- fontSize: 13,
597
- color: _textSecondary,
608
+ ],
609
+ if (session.structuredContent['events'] != null &&
610
+ _getStructuredList(
611
+ session,
612
+ 'events',
613
+ ).isNotEmpty) ...<Widget>[
614
+ const SizedBox(height: 10),
615
+ Text(
616
+ 'Events Mentioned',
617
+ style: TextStyle(
618
+ fontWeight: FontWeight.w700,
619
+ fontSize: 13,
620
+ color: _textSecondary,
621
+ ),
598
622
  ),
599
- ),
600
- const SizedBox(height: 4),
601
- ..._getStructuredList(session, 'events').map(
602
- (item) => Padding(
603
- padding: const EdgeInsets.only(bottom: 4),
604
- child: Row(
605
- crossAxisAlignment: CrossAxisAlignment.start,
606
- children: [
607
- Text(
608
- '• ',
609
- style: TextStyle(
610
- fontWeight: FontWeight.w700,
611
- color: _accent,
623
+ const SizedBox(height: 4),
624
+ ..._getStructuredList(session, 'events').map(
625
+ (item) => Padding(
626
+ padding: const EdgeInsets.only(bottom: 4),
627
+ child: Row(
628
+ crossAxisAlignment: CrossAxisAlignment.start,
629
+ children: [
630
+ Text(
631
+ '• ',
632
+ style: TextStyle(
633
+ fontWeight: FontWeight.w700,
634
+ color: _accent,
635
+ ),
612
636
  ),
613
- ),
614
- Expanded(
615
- child: Text(
616
- item.toString(),
617
- style: TextStyle(height: 1.35),
637
+ Expanded(
638
+ child: Text(
639
+ item.toString(),
640
+ style: TextStyle(height: 1.35),
641
+ ),
618
642
  ),
619
- ),
620
- ],
643
+ ],
644
+ ),
621
645
  ),
622
646
  ),
623
- ),
647
+ ],
624
648
  ],
625
- ],
649
+ ),
626
650
  ),
627
- ),
628
- ],
629
- if (session.transcriptSegments.isNotEmpty) ...<Widget>[
630
- const SizedBox(height: 16),
631
- ...session.transcriptSegments.map(
632
- (segment) => Padding(
633
- padding: const EdgeInsets.only(bottom: 10),
634
- child: Row(
635
- crossAxisAlignment: CrossAxisAlignment.start,
636
- children: <Widget>[
637
- SizedBox(
638
- width: 88,
639
- child: Text(
640
- segment.timestampLabel,
641
- style: TextStyle(color: _textSecondary),
642
- ),
643
- ),
644
- Expanded(
645
- child: Text(
646
- segment.displayText,
647
- style: TextStyle(height: 1.45),
648
- ),
651
+ ],
652
+ if (session.transcriptSegments.isNotEmpty) ...<Widget>[
653
+ const SizedBox(height: 16),
654
+ ...session.transcriptSegments.map(
655
+ (segment) => Padding(
656
+ padding: const EdgeInsets.only(bottom: 10),
657
+ child: _GlassSurface(
658
+ padding: const EdgeInsets.fromLTRB(14, 12, 10, 12),
659
+ borderRadius: BorderRadius.circular(16),
660
+ blurSigma: 6,
661
+ fillColor: _bgCard.withValues(alpha: 0.9),
662
+ borderColor: _border,
663
+ child: Row(
664
+ crossAxisAlignment: CrossAxisAlignment.start,
665
+ children: <Widget>[
666
+ SizedBox(
667
+ width: 88,
668
+ child: Text(
669
+ segment.timestampLabel,
670
+ style: TextStyle(
671
+ color: _textSecondary,
672
+ fontWeight: FontWeight.w600,
673
+ ),
674
+ ),
675
+ ),
676
+ Expanded(
677
+ child: Text(
678
+ segment.displayText,
679
+ style: const TextStyle(
680
+ height: 1.55,
681
+ fontSize: 15,
682
+ ),
683
+ ),
684
+ ),
685
+ if (onDeleteSegment != null &&
686
+ segment.id > 0) ...<Widget>[
687
+ const SizedBox(width: 8),
688
+ IconButton(
689
+ onPressed: () async {
690
+ await onDeleteSegment!(segment);
691
+ },
692
+ icon: Icon(Icons.delete_outline),
693
+ tooltip: 'Delete segment',
694
+ visualDensity: VisualDensity.compact,
695
+ ),
696
+ ],
697
+ ],
649
698
  ),
650
- if (onDeleteSegment != null &&
651
- segment.id > 0) ...<Widget>[
652
- const SizedBox(width: 8),
653
- IconButton(
654
- onPressed: () async {
655
- await onDeleteSegment!(segment);
656
- },
657
- icon: Icon(Icons.delete_outline),
658
- tooltip: 'Delete segment',
659
- visualDensity: VisualDensity.compact,
660
- ),
661
- ],
662
- ],
699
+ ),
663
700
  ),
664
701
  ),
665
- ),
666
- ] else if (session.transcriptText.isNotEmpty) ...<Widget>[
667
- const SizedBox(height: 16),
668
- Container(
669
- width: double.infinity,
670
- padding: const EdgeInsets.all(14),
671
- decoration: BoxDecoration(
672
- color: _bgSecondary,
673
- borderRadius: BorderRadius.circular(14),
674
- border: Border.all(color: _border),
702
+ ] else if (session.transcriptText.isNotEmpty) ...<Widget>[
703
+ const SizedBox(height: 16),
704
+ _GlassSurface(
705
+ width: double.infinity,
706
+ padding: const EdgeInsets.all(16),
707
+ borderRadius: BorderRadius.circular(16),
708
+ blurSigma: 8,
709
+ fillColor: _bgCard.withValues(alpha: 0.92),
710
+ borderColor: _border,
711
+ child: SelectableText(
712
+ session.transcriptText,
713
+ style: const TextStyle(height: 1.6, fontSize: 15),
714
+ ),
675
715
  ),
676
- child: SelectableText(
677
- session.transcriptText,
678
- style: TextStyle(height: 1.45),
716
+ ] else ...<Widget>[
717
+ const SizedBox(height: 16),
718
+ Text(
719
+ session.status == 'processing'
720
+ ? 'Transcribing...'
721
+ : session.status == 'failed'
722
+ ? 'Transcription failed. Check the error above and retry.'
723
+ : session.status == 'completed'
724
+ ? 'No transcript text was returned. You can retry transcription.'
725
+ : 'Transcript is not available yet.',
726
+ style: TextStyle(color: _textSecondary),
679
727
  ),
680
- ),
681
- ] else ...<Widget>[
682
- const SizedBox(height: 16),
683
- Text(
684
- session.status == 'processing'
685
- ? 'Transcribing...'
686
- : session.status == 'failed'
687
- ? 'Transcription failed. Check the error above and retry.'
688
- : session.status == 'completed'
689
- ? 'No transcript text was returned. You can retry transcription.'
690
- : 'Transcript is not available yet.',
691
- style: TextStyle(color: _textSecondary),
692
- ),
693
- ],
694
- if (onRetry != null || canDeleteRecording) ...<Widget>[
695
- const SizedBox(height: 14),
696
- Wrap(
697
- spacing: 10,
698
- runSpacing: 10,
699
- children: <Widget>[
700
- if (onRetry != null)
701
- OutlinedButton.icon(
702
- onPressed: onRetry,
703
- icon: Icon(Icons.replay),
704
- label: Text('Retry transcription'),
705
- ),
706
- if (canDeleteRecording)
707
- OutlinedButton.icon(
708
- onPressed: () async {
709
- await onDeleteRecording!();
710
- },
711
- icon: Icon(Icons.delete_forever_outlined),
712
- label: Text('Delete recording'),
713
- style: OutlinedButton.styleFrom(foregroundColor: _danger),
714
- ),
715
- ],
716
- ),
728
+ ],
729
+ if (onRetry != null || canDeleteRecording) ...<Widget>[
730
+ const SizedBox(height: 14),
731
+ Wrap(
732
+ spacing: 10,
733
+ runSpacing: 10,
734
+ children: <Widget>[
735
+ if (onRetry != null)
736
+ OutlinedButton.icon(
737
+ onPressed: onRetry,
738
+ icon: Icon(Icons.replay),
739
+ label: Text('Retry transcription'),
740
+ ),
741
+ if (canDeleteRecording)
742
+ OutlinedButton.icon(
743
+ onPressed: () async {
744
+ await onDeleteRecording!();
745
+ },
746
+ icon: Icon(Icons.delete_forever_outlined),
747
+ label: Text('Delete recording'),
748
+ style: OutlinedButton.styleFrom(
749
+ foregroundColor: _danger,
750
+ ),
751
+ ),
752
+ ],
753
+ ),
754
+ ],
717
755
  ],
718
- ],
756
+ ),
719
757
  ),
720
758
  ),
721
759
  );
@@ -857,11 +895,23 @@ class _RecordingSourceAudioControlsState
857
895
  runSpacing: 8,
858
896
  children: audioSources.map((source) {
859
897
  final isActive = _isPlaying && _activeSourceKey == source.sourceKey;
860
- return OutlinedButton.icon(
861
- onPressed: () => _toggleSource(source),
862
- icon: Icon(isActive ? Icons.stop_circle_outlined : Icons.play_arrow),
863
- label: Text(
864
- isActive ? 'Stop ${source.label}' : 'Play ${source.label}',
898
+ return AnimatedScale(
899
+ duration: const Duration(milliseconds: 180),
900
+ curve: Curves.easeOutCubic,
901
+ scale: isActive ? 1.02 : 1,
902
+ child: OutlinedButton.icon(
903
+ onPressed: () => _toggleSource(source),
904
+ icon: Icon(
905
+ isActive ? Icons.stop_circle_outlined : Icons.play_arrow,
906
+ ),
907
+ label: Text(
908
+ isActive ? 'Stop ${source.label}' : 'Play ${source.label}',
909
+ ),
910
+ style: OutlinedButton.styleFrom(
911
+ backgroundColor: isActive
912
+ ? _accentMuted.withValues(alpha: 0.2)
913
+ : null,
914
+ ),
865
915
  ),
866
916
  );
867
917
  }).toList(),