import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/music_player_provider.dart'; import 'package:spotiflac_android/utils/synced_lyrics_scroll.dart'; /// Playback controls the fill; a gentle pulse accompanies instrumental gaps. class LyricGapIndicator extends ConsumerStatefulWidget { const LyricGapIndicator({ super.key, required this.start, required this.end, required this.color, this.position, }); final Duration start; final Duration end; final Color color; final Duration? position; @override ConsumerState createState() => _LyricGapIndicatorState(); } class _LyricGapIndicatorState extends ConsumerState with SingleTickerProviderStateMixin { late final _pulse = AnimationController( vsync: this, duration: const Duration(milliseconds: 3600), ); @override void dispose() { _pulse.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Duration position = widget.position ?? ref.watch(playbackPositionProvider); final progress = syncedLyricSegmentProgress( position: position, start: widget.start, end: widget.end, ); final reduceMotion = MediaQuery.disableAnimationsOf(context); final animate = ref.watch(playbackPlayingProvider) && !ref.watch(playbackLoadingProvider) && !reduceMotion && widget.position == null && position >= widget.start && position < widget.end; if (animate) { if (!_pulse.isAnimating) _pulse.repeat(); } else { _pulse.stop(); } return Semantics( label: context.l10n.nowPlayingInstrumental, value: '${(progress * 100).round()}%', child: Row( mainAxisSize: MainAxisSize.min, children: [ for (var index = 0; index < 3; index++) ...[ if (index > 0) const SizedBox(width: 8), AnimatedBuilder( animation: _pulse, builder: (context, child) { // All three dots breathe together; only their fill advances // independently with the playback position. final breath = math.sin(math.pi * _pulse.value); return Transform.scale( scale: reduceMotion || widget.position != null ? 1 : 1 + 0.24 * breath * breath, child: child, ); }, child: AnimatedContainer( key: ValueKey('lyric-gap-dot-$index'), duration: animate ? const Duration(milliseconds: 120) : Duration.zero, width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: Color.lerp( widget.color.withValues(alpha: 0.25), widget.color, (progress * 3 - index).clamp(0.0, 1.0), ), ), ), ), ], ], ), ); } }