Files
plezy/lib/widgets/music/equalizer_icon.dart
T
edde746 7d6a747aa7 feat(music): now-playing, mini-player, queue and lyrics UI
Persistent mini-player over the profile navigator (route-aware
suppression, bottom-bar inset), slide-up now-playing screen with
mobile/desktop/TV layouts (blurred-art background, pause shape morphs,
dpad seek chain), queue sheet with reorder/remove/jump, synced lyrics
view, TV rail now-playing item, sleep timer, and audio playlist
playback. Adds download affordances on music surfaces.
2026-07-05 21:50:11 +02:00

110 lines
3.1 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../services/device_performance.dart';
/// Small 3-bar "now playing" indicator. Bars animate while [animate] is true;
/// on the reduced visual-effects tier they render static regardless (each
/// animation frame re-rasterizes the host row on weak TV GPUs).
///
/// Shared between [TrackRow]'s leading column, the queue sheet's current-track
/// header, and the side navigation rail's Now Playing item.
class EqualizerIcon extends StatefulWidget {
final bool animate;
final Color color;
const EqualizerIcon({super.key, required this.animate, required this.color});
@override
State<EqualizerIcon> createState() => _EqualizerIconState();
}
class _EqualizerIconState extends State<EqualizerIcon> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
bool get _shouldAnimate => widget.animate && !DevicePerformance.isReduced;
@override
void initState() {
super.initState();
_syncAnimation();
}
@override
void didUpdateWidget(EqualizerIcon oldWidget) {
super.didUpdateWidget(oldWidget);
_syncAnimation();
}
void _syncAnimation() {
if (_shouldAnimate) {
if (!_controller.isAnimating) _controller.repeat();
} else {
_controller.stop();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: 16,
height: 14,
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) => CustomPaint(
painter: _EqualizerPainter(t: _controller.value, color: widget.color, animate: _shouldAnimate),
),
),
);
}
}
class _EqualizerPainter extends CustomPainter {
final double t;
final Color color;
final bool animate;
/// Static bar heights (fraction of full height) for the paused/reduced look.
static const List<double> _staticHeights = [0.55, 0.9, 0.4];
/// Per-bar phase offsets so the animated bars move out of step.
static const List<double> _phases = [0.0, 0.35, 0.7];
const _EqualizerPainter({required this.t, required this.color, required this.animate});
@override
void paint(Canvas canvas, Size size) {
const barCount = 3;
const gap = 2.5;
final barWidth = (size.width - gap * (barCount - 1)) / barCount;
final paint = Paint()..color = color;
for (var i = 0; i < barCount; i++) {
final fraction = animate ? 0.3 + 0.7 * (0.5 + 0.5 * math.sin(2 * math.pi * (t + _phases[i]))) : _staticHeights[i];
final barHeight = size.height * fraction;
final left = i * (barWidth + gap);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(left, size.height - barHeight, barWidth, barHeight),
const Radius.circular(1.5),
),
paint,
);
}
}
@override
bool shouldRepaint(_EqualizerPainter oldDelegate) =>
oldDelegate.t != t || oldDelegate.color != color || oldDelegate.animate != animate;
}