import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../player/audio_effects.dart'; import '../shared/theme.dart'; /// Equalizer-Bildschirm: Klangprofile, manuelle Bänder und die beiden /// Drehregler für Bass und Immersives Audio. class EqualizerScreen extends StatelessWidget { const EqualizerScreen({super.key}); @override Widget build(BuildContext context) { final effects = context.watch(); return Scaffold( appBar: AppBar( title: const Text('Equalizer'), actions: [ Padding( padding: const EdgeInsets.only(right: 12), child: Switch( value: effects.enabled, onChanged: effects.available ? effects.setEnabled : null, ), ), ], ), body: !effects.available ? _NotAvailable(error: effects.error) : Opacity( opacity: effects.enabled ? 1 : 0.4, child: IgnorePointer( ignoring: !effects.enabled, child: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), children: [ _Presets(effects: effects), const SizedBox(height: 24), const _SectionTitle('Manuelle Anpassung'), const SizedBox(height: 8), _Bands(effects: effects), const SizedBox(height: 28), const _SectionTitle('Sound verbessern'), const Padding( padding: EdgeInsets.only(top: 4, bottom: 12), child: Row( children: [ Icon(Icons.headphones, size: 14, color: Colors.white38), SizedBox(width: 6), Expanded( child: Text( 'Kopfhörer aufsetzen, bevor du die Soundeffekte ' 'einstellst', style: TextStyle( color: Colors.white38, fontSize: 12), ), ), ], ), ), _Enhancers(effects: effects), ], ), ), ), ); } } class _NotAvailable extends StatelessWidget { const _NotAvailable({this.error}); final String? error; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.graphic_eq, size: 64, color: Colors.white24), const SizedBox(height: 12), const Text( 'Der Equalizer ist erst verfügbar, wenn Musik läuft', textAlign: TextAlign.center, style: TextStyle(color: Colors.white54), ), if (error != null) ...[ const SizedBox(height: 8), Text(error!, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white24, fontSize: 12)), ], ], ), ), ); } } class _SectionTitle extends StatelessWidget { const _SectionTitle(this.text); final String text; @override Widget build(BuildContext context) { return Text(text, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700)); } } class _Presets extends StatelessWidget { const _Presets({required this.effects}); final AudioEffects effects; @override Widget build(BuildContext context) { return Wrap( spacing: 10, runSpacing: 10, children: [ for (var i = 0; i < effects.presets.length; i++) SizedBox( width: (MediaQuery.of(context).size.width - 32 - 20) / 3, height: 64, child: Material( color: MeloTheme.surfaceHigh, borderRadius: BorderRadius.circular(10), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: () => effects.usePreset(i), child: Center( child: Text(effects.presets[i], textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis), ), ), ), ), ], ); } } class _Bands extends StatelessWidget { const _Bands({required this.effects}); final AudioEffects effects; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ for (final band in effects.bands) Expanded( child: Column( children: [ SizedBox( height: 200, child: RotatedBox( quarterTurns: 3, child: Slider( value: band.level .clamp(effects.minLevel, effects.maxLevel) .toDouble(), min: effects.minLevel.toDouble(), max: effects.maxLevel.toDouble(), onChanged: (v) => effects.setBandLevel(band.index, v.round()), ), ), ), Text(band.label, style: const TextStyle(color: Colors.white54, fontSize: 11)), Text('${(band.level / 100).round()}', style: const TextStyle(color: Colors.white38, fontSize: 11)), ], ), ), ], ); } } class _Enhancers extends StatelessWidget { const _Enhancers({required this.effects}); final AudioEffects effects; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Knob( label: 'Bass', value: effects.bass / 1000, enabled: effects.bassSupported, onChanged: (v) => effects.setBass((v * 1000).round()), ), Knob( label: 'Immersives Audio', value: effects.virtualizer / 1000, enabled: effects.virtualizerSupported, onChanged: (v) => effects.setVirtualizer((v * 1000).round()), ), ], ); } } /// Drehregler wie in der Vorlage: senkrecht wischen regelt von 0 bis 1. class Knob extends StatelessWidget { const Knob({ super.key, required this.label, required this.value, required this.onChanged, this.enabled = true, }); final String label; final double value; final ValueChanged onChanged; final bool enabled; static const _size = 120.0; @override Widget build(BuildContext context) { return Opacity( opacity: enabled ? 1 : 0.4, child: Column( children: [ GestureDetector( onVerticalDragUpdate: enabled // Ein voller Ausschlag entspricht ~200 Pixel Wischweg. ? (d) => onChanged((value - d.delta.dy / 200).clamp(0.0, 1.0)) : null, child: CustomPaint( size: const Size(_size, _size), painter: _KnobPainter(value), ), ), const SizedBox(height: 8), Text(label, style: const TextStyle(color: Colors.white54, fontSize: 12)), Text('${(value * 100).round()}', style: const TextStyle(color: Colors.white38, fontSize: 11)), ], ), ); } } class _KnobPainter extends CustomPainter { _KnobPainter(this.value); final double value; /// Der Regelbereich läuft von unten links (135°) über 270° nach unten rechts. static const _start = math.pi * 0.75; static const _sweep = math.pi * 1.5; static const _dots = 24; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2; for (var i = 0; i < _dots; i++) { final t = i / (_dots - 1); final angle = _start + _sweep * t; final active = t <= value; canvas.drawCircle( center + Offset(math.cos(angle), math.sin(angle)) * (radius - 5), active ? 3.5 : 2.5, Paint()..color = active ? MeloTheme.red : Colors.white24, ); } canvas.drawCircle( center, radius - 16, Paint()..color = MeloTheme.surfaceHigh, ); canvas.drawCircle( center, radius - 16, Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1 ..color = Colors.white12, ); final pointer = _start + _sweep * value; canvas.drawLine( center + Offset(math.cos(pointer), math.sin(pointer)) * (radius - 46), center + Offset(math.cos(pointer), math.sin(pointer)) * (radius - 22), Paint() ..color = Colors.white ..strokeWidth = 2 ..strokeCap = StrokeCap.round, ); } @override bool shouldRepaint(_KnobPainter oldDelegate) => oldDelegate.value != value; }