import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import 'package:go_router/go_router.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../app/icons/app_icons.dart'; import '../../../app/theme/app_colors.dart'; import '../../../core/services/app_audio_player.dart'; import '../../../data/services/muslim_api_service.dart'; import '../../../data/services/unsplash_service.dart'; /// Quran Murattal (audio player) screen. /// Implements full Surah playback using just_audio. class QuranMurattalScreen extends ConsumerStatefulWidget { final String surahId; final String? initialQariId; final bool autoPlay; final bool isSimpleModeTab; const QuranMurattalScreen({ super.key, required this.surahId, this.initialQariId, this.autoPlay = false, this.isSimpleModeTab = false, }); @override ConsumerState createState() => _QuranMurattalScreenState(); } class _QuranMurattalScreenState extends ConsumerState with SingleTickerProviderStateMixin { final AudioPlayer _audioPlayer = AppAudioPlayer.instance; Map? _surahData; bool _isLoading = true; // Audio State Variables bool _isPlaying = false; bool _isBuffering = false; Duration _position = Duration.zero; Duration _duration = Duration.zero; StreamSubscription? _positionSub; StreamSubscription? _durationSub; StreamSubscription? _playerStateSub; // Qari State late String _selectedQariId; // Shuffle State bool _isShuffleEnabled = false; // Unsplash Background Map? _unsplashPhoto; late final AnimationController _goldRingController; @override void initState() { super.initState(); _selectedQariId = widget.initialQariId ?? '05'; // Default to Misyari Rasyid Al-Afasi _goldRingController = AnimationController( vsync: this, duration: const Duration(milliseconds: 5000), )..repeat(); _initDataAndPlayer(); _loadUnsplashPhoto(); } Future _loadUnsplashPhoto() async { final photo = await UnsplashService.instance.getIslamicPhoto(); if (mounted && photo != null) { setState(() => _unsplashPhoto = photo); } } Future _initDataAndPlayer() async { final surahNum = int.tryParse(widget.surahId) ?? 1; final data = await MuslimApiService.instance.getSurah(surahNum); if (data != null && mounted) { setState(() { _surahData = data; _isLoading = false; }); _setupAudioStreamListeners(); _loadAudioSource(); } else if (mounted) { setState(() => _isLoading = false); } } void _setupAudioStreamListeners() { _positionSub = _audioPlayer.positionStream.listen((pos) { if (mounted) setState(() => _position = pos); }); _durationSub = _audioPlayer.durationStream.listen((dur) { if (mounted && dur != null) setState(() => _duration = dur); }); _playerStateSub = _audioPlayer.playerStateStream.listen((state) { if (!mounted) return; setState(() { _isPlaying = state.playing; _isBuffering = state.processingState == ProcessingState.buffering || state.processingState == ProcessingState.loading; // Auto pause and reset to 0 when finished if (state.processingState == ProcessingState.completed) { _audioPlayer.pause(); _audioPlayer.seek(Duration.zero); // Auto-play next surah final currentSurah = int.tryParse(widget.surahId) ?? 1; if (_isShuffleEnabled) { final random = Random(); int nextSurah = random.nextInt(114) + 1; while (nextSurah == currentSurah) { nextSurah = random.nextInt(114) + 1; } _navigateToSurahNumber(nextSurah, autoplay: true); } else if (currentSurah < 114) { _navigateToSurah(1); } } }); }); } Future _loadAudioSource() async { if (_surahData == null) return; final audioUrls = _surahData!['audioFull']; if (audioUrls != null && audioUrls[_selectedQariId] != null) { try { final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}'; final qariName = MuslimApiService.qariNames[_selectedQariId] ?? 'Qari'; final imageUrl = _unsplashPhoto?['imageUrl']; await _audioPlayer.setAudioSource( AudioSource.uri( Uri.parse(audioUrls[_selectedQariId] as String), tag: MediaItem( id: 'murattal_${widget.surahId}_$_selectedQariId', album: "Al-Qur'an Murattal", title: 'Surah $surahName', artist: qariName, artUri: imageUrl != null ? Uri.tryParse(imageUrl) : null, ), ), ); if (widget.autoPlay) { _audioPlayer.play(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gagal memuat audio murattal')), ); } } } } @override void dispose() { _positionSub?.cancel(); _durationSub?.cancel(); _playerStateSub?.cancel(); _goldRingController.dispose(); super.dispose(); } void _syncGoldRingAnimation({required bool reducedMotion}) { if (reducedMotion) { if (_goldRingController.isAnimating) { _goldRingController.stop(canceled: false); } return; } if (!_goldRingController.isAnimating) { _goldRingController.repeat(); } } String _formatDuration(Duration d) { final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0'); if (d.inHours > 0) { return '${d.inHours}:$minutes:$seconds'; } return '$minutes:$seconds'; } void _seekRelative(int seconds) { final newPosition = _position + Duration(seconds: seconds); if (newPosition < Duration.zero) { _audioPlayer.seek(Duration.zero); } else if (newPosition > _duration) { _audioPlayer.seek(_duration); } else { _audioPlayer.seek(newPosition); } } void _navigateToSurah(int direction) { final currentSurah = int.tryParse(widget.surahId) ?? 1; final nextSurah = currentSurah + direction; _navigateToSurahNumber(nextSurah, autoplay: true); } void _navigateToSurahNumber(int surahNum, {bool autoplay = false}) { if (surahNum >= 1 && surahNum <= 114) { final base = widget.isSimpleModeTab ? '/quran' : '/tools/quran'; context.pushReplacement( '$base/$surahNum/murattal?qariId=$_selectedQariId&autoplay=$autoplay', ); } } void _navigateToQuranReading() { final base = widget.isSimpleModeTab ? '/quran' : '/tools/quran'; context.push('$base/${widget.surahId}'); } void _showQariSelector() { showModalBottomSheet( context: context, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), const Text( 'Pilih Qari', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), ...MuslimApiService.qariNames.entries.map((entry) { final isSelected = entry.key == _selectedQariId; return ListTile( leading: AppIcon( glyph: isSelected ? AppIcons.checkCircle : AppIcons.circle, color: isSelected ? AppColors.primary : Colors.grey, ), title: Text( entry.value, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? AppColors.primary : null, ), ), onTap: () { Navigator.pop(context); if (!isSelected) { setState(() => _selectedQariId = entry.key); _loadAudioSource(); } }, ); }), const SizedBox(height: 16), ], ), ); }, ); } void _showSurahPlaylist() { final currentSurah = int.tryParse(widget.surahId) ?? 1; showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return DraggableScrollableSheet( initialChildSize: 0.7, minChildSize: 0.4, maxChildSize: 0.9, expand: false, builder: (context, scrollController) { return Column( children: [ const SizedBox(height: 12), Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), const Text( 'Playlist Surah', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Expanded( child: FutureBuilder>>( future: MuslimApiService.instance.getAllSurahs(), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final surahs = snapshot.data!; return ListView.builder( controller: scrollController, itemCount: surahs.length, itemBuilder: (context, i) { final surah = surahs[i]; final surahNum = surah['nomor'] ?? (i + 1); final isCurrentSurah = surahNum == currentSurah; return ListTile( leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: isCurrentSurah ? AppColors.primary : AppColors.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Center( child: Text( '$surahNum', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isCurrentSurah ? Colors.white : AppColors.primary, ), ), ), ), title: Text( surah['namaLatin'] ?? 'Surah $surahNum', style: TextStyle( fontWeight: isCurrentSurah ? FontWeight.bold : FontWeight.normal, color: isCurrentSurah ? AppColors.primary : null, ), ), subtitle: Text( '${surah['arti'] ?? ''} • ${surah['jumlahAyat'] ?? 0} Ayat', style: const TextStyle(fontSize: 12), ), trailing: isCurrentSurah ? const AppIcon( glyph: AppIcons.musicNote, color: AppColors.primary, size: 20, ) : null, onTap: () { Navigator.pop(context); if (!isCurrentSurah) { context.pushReplacement( '${widget.isSimpleModeTab ? '/quran' : '/tools/quran'}/$surahNum/murattal?qariId=$_selectedQariId', ); } }, ); }, ); }, ), ), ], ); }, ); }, ); } @override Widget build(BuildContext context) { final media = MediaQuery.maybeOf(context); final reducedMotion = (media?.disableAnimations ?? false) || (media?.accessibleNavigation ?? false); _syncGoldRingAnimation(reducedMotion: reducedMotion); final isDark = Theme.of(context).brightness == Brightness.dark; final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}'; final systemBottomInset = media?.viewPadding.bottom ?? 0.0; final playerBottomPadding = 32 + systemBottomInset; final playerReservedBottom = 280 + systemBottomInset; final hasPhoto = _unsplashPhoto != null; return Scaffold( extendBodyBehindAppBar: hasPhoto, appBar: AppBar( leading: IconButton( icon: AppIcon( glyph: AppIcons.backArrow, color: hasPhoto ? Colors.white : null, ), onPressed: () { if (widget.isSimpleModeTab) { context.go('/'); } else { context.pop(); } }, ), actions: [ IconButton( icon: AppIcon( glyph: AppIcons.quran, color: hasPhoto ? Colors.white : null, ), tooltip: 'Buka Surah', onPressed: _navigateToQuranReading, ), ], backgroundColor: hasPhoto ? Colors.transparent : null, elevation: hasPhoto ? 0 : null, iconTheme: hasPhoto ? const IconThemeData(color: Colors.white) : null, flexibleSpace: hasPhoto ? ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( color: Colors.black.withValues(alpha: 0.2), ), ), ) : null, title: Column( children: [ Text( 'Surah $surahName', style: TextStyle( color: hasPhoto ? Colors.white : null, ), ), Text( 'MURATTAL', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 1.5, color: hasPhoto ? Colors.white70 : AppColors.primary, ), ), ], ), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : Stack( fit: StackFit.expand, children: [ // === FULL-BLEED BACKGROUND === if (_unsplashPhoto != null) CachedNetworkImage( imageUrl: _unsplashPhoto!['imageUrl'] ?? '', fit: BoxFit.cover, placeholder: (context, url) => Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.primary.withValues(alpha: 0.1), AppColors.primary.withValues(alpha: 0.05), ], ), ), ), errorWidget: (context, url, error) => Container( color: isDark ? Colors.black : Colors.grey.shade100, ), ) else Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.primary.withValues(alpha: 0.1), AppColors.primary.withValues(alpha: 0.03), ], ), ), ), // Dark overlay if (_unsplashPhoto != null) Container( color: Colors.black.withValues(alpha: 0.35), ), // === CENTER CONTENT (Equalizer + Text) === Positioned( top: 0, left: 0, right: 0, bottom: playerReservedBottom, // leave room for player child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Equalizer circle Container( width: 220, height: 220, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: _unsplashPhoto != null ? [ Colors.white.withValues(alpha: 0.15), Colors.white.withValues(alpha: 0.05), ] : [ AppColors.primary.withValues(alpha: 0.2), AppColors.primary.withValues(alpha: 0.05), ], ), ), child: Center( child: Container( width: 140, height: 140, decoration: BoxDecoration( shape: BoxShape.circle, color: _unsplashPhoto != null ? Colors.white.withValues(alpha: 0.1) : AppColors.primary.withValues(alpha: 0.12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: List.generate(7, (i) { return Padding( padding: const EdgeInsets.symmetric( horizontal: 2), child: _EqualizerBar( isPlaying: _isPlaying, index: i, color: _unsplashPhoto != null ? Colors.white : AppColors.primary, ), ); }), ), ), ), ), const SizedBox(height: 32), // Qari name Text( MuslimApiService.qariNames[_selectedQariId] ?? 'Memuat...', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _unsplashPhoto != null ? Colors.white : null, ), ), const SizedBox(height: 4), Text( 'Memutar Surat $surahName', style: TextStyle( fontSize: 14, color: _unsplashPhoto != null ? Colors.white70 : AppColors.primary, ), ), ], ), ), ), // === FROSTED GLASS PLAYER CONTROLS === Positioned( bottom: 0, left: 0, right: 0, child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), child: BackdropFilter( filter: ImageFilter.blur( sigmaX: _unsplashPhoto != null ? 20 : 14, sigmaY: _unsplashPhoto != null ? 20 : 14, ), child: Container( padding: EdgeInsets.fromLTRB( 24, 16, 24, playerBottomPadding), decoration: BoxDecoration( color: AppColors.primary.withValues( alpha: _unsplashPhoto != null ? 0.22 : (isDark ? 0.18 : 0.14), ), borderRadius: const BorderRadius.vertical( top: Radius.circular(24)), border: Border( top: BorderSide( color: _unsplashPhoto != null ? Colors.white.withValues(alpha: 0.24) : AppColors.primary.withValues(alpha: 0.32), width: 0.7, ), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Progress slider SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 3, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6), ), child: Slider( value: _position.inMilliseconds.toDouble(), max: _duration.inMilliseconds > 0 ? _duration.inMilliseconds.toDouble() : 1.0, onChanged: (v) { _audioPlayer .seek(Duration(milliseconds: v.round())); }, activeColor: _unsplashPhoto != null ? Colors.white : AppColors.primary, inactiveColor: _unsplashPhoto != null ? Colors.white.withValues(alpha: 0.2) : AppColors.primary.withValues(alpha: 0.15), ), ), // Time labels Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _formatDuration(_position), style: TextStyle( fontSize: 12, color: _unsplashPhoto != null ? Colors.white70 : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), ), ), Text( _formatDuration(_duration), style: TextStyle( fontSize: 12, color: _unsplashPhoto != null ? Colors.white70 : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), ), ), ], ), ), const SizedBox(height: 16), // Playback controls — Spotify-style 5-button row Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Shuffle IconButton( onPressed: () => setState(() => _isShuffleEnabled = !_isShuffleEnabled), icon: AppIcon( glyph: AppIcons.shuffle, size: 24, color: _isShuffleEnabled ? (_unsplashPhoto != null ? Colors.white : AppColors.primary) : (_unsplashPhoto != null ? Colors.white54 : (isDark ? AppColors.textSecondaryDark : AppColors .textSecondaryLight)), ), ), // Previous Surah IconButton( onPressed: (int.tryParse(widget.surahId) ?? 1) > 1 ? () => _navigateToSurah(-1) : null, icon: AppIcon( glyph: AppIcons.previousTrack, size: 36, color: (int.tryParse(widget.surahId) ?? 1) > 1 ? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)) : Colors.grey.withValues(alpha: 0.2), ), ), // Play/Pause GestureDetector( onTap: () { if (_isPlaying) { _audioPlayer.pause(); } else { _audioPlayer.play(); } }, child: SizedBox( width: 68, height: 68, child: RepaintBoundary( child: AnimatedBuilder( animation: _goldRingController, builder: (_, __) { final ringProgress = reducedMotion ? 0.18 : _goldRingController.value; return CustomPaint( painter: _MurattalGoldRingPainter( progress: ringProgress, reducedMotion: reducedMotion, isDark: isDark, ), child: Padding( padding: const EdgeInsets.all(3), child: DecoratedBox( decoration: BoxDecoration( color: _unsplashPhoto != null ? AppColors.brandTeal900 .withValues( alpha: 0.88) : AppColors.primary, shape: BoxShape.circle, ), child: _isBuffering ? Padding( padding: const EdgeInsets .all(18.0), child: CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ) : Padding( padding: const EdgeInsets .all(18), child: AppIcon( glyph: _isPlaying ? AppIcons.pause : AppIcons .murattal, size: 18, color: AppColors .onPrimary, ), ), ), ), ); }, ), ), ), ), // Next Surah IconButton( onPressed: (int.tryParse(widget.surahId) ?? 1) < 114 ? () => _navigateToSurah(1) : null, icon: AppIcon( glyph: AppIcons.nextTrack, size: 36, color: (int.tryParse(widget.surahId) ?? 1) < 114 ? (_unsplashPhoto != null ? Colors.white : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight)) : Colors.grey.withValues(alpha: 0.2), ), ), // Playlist IconButton( onPressed: _showSurahPlaylist, icon: AppIcon( glyph: AppIcons.playlist, size: 28, color: _unsplashPhoto != null ? Colors.white70 : (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight), ), ), ], ), const SizedBox(height: 16), // Qari selector trigger GestureDetector( onTap: _showQariSelector, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10), decoration: BoxDecoration( color: _unsplashPhoto != null ? Colors.white.withValues(alpha: 0.15) : AppColors.primary .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(50), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ AppIcon( glyph: AppIcons.user, size: 16, color: _unsplashPhoto != null ? Colors.white : AppColors.primary, ), const SizedBox(width: 8), Text( MuslimApiService .qariNames[_selectedQariId] ?? 'Ganti Qari', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _unsplashPhoto != null ? Colors.white : AppColors.primary, ), ), const SizedBox(width: 4), AppIcon( glyph: AppIcons.arrowDown, size: 16, color: _unsplashPhoto != null ? Colors.white : AppColors.primary, ), ], ), ), ), ], ), ), ), ), ), // === ATTRIBUTION === if (_unsplashPhoto != null) Positioned( bottom: playerReservedBottom, left: 0, right: 0, child: GestureDetector( onTap: () { final url = _unsplashPhoto!['photographerUrl']; if (url != null && url.isNotEmpty) { launchUrl(Uri.parse( '$url?utm_source=jamshalat&utm_medium=referral')); } }, child: Text( '📷 ${_unsplashPhoto!['photographerName']} / Unsplash', textAlign: TextAlign.center, style: TextStyle( fontSize: 10, color: Colors.white.withValues(alpha: 0.6), fontWeight: FontWeight.w500, ), ), ), ), ], ), ); } } class _MurattalGoldRingPainter extends CustomPainter { const _MurattalGoldRingPainter({ required this.progress, required this.reducedMotion, required this.isDark, }); final double progress; final bool reducedMotion; final bool isDark; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final outerRadius = (size.shortestSide / 2) - 0.8; final ringRadius = (size.shortestSide / 2) - 2.0; final innerRadius = (size.shortestSide / 2) - 3.8; final rotation = reducedMotion ? pi * 0.63 : progress * pi * 2; void drawEmboss({ required double radius, required Offset shadowOffset, required Offset highlightOffset, required Color shadowColor, required Color highlightColor, required double shadowBlur, required double highlightBlur, required double strokeWidth, }) { final shadowPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..color = shadowColor ..maskFilter = MaskFilter.blur(BlurStyle.normal, shadowBlur); final highlightPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..color = highlightColor ..maskFilter = MaskFilter.blur(BlurStyle.normal, highlightBlur); canvas.save(); canvas.translate(shadowOffset.dx, shadowOffset.dy); canvas.drawCircle(center, radius, shadowPaint); canvas.restore(); canvas.save(); canvas.translate(highlightOffset.dx, highlightOffset.dy); canvas.drawCircle(center, radius, highlightPaint); canvas.restore(); } drawEmboss( radius: outerRadius, shadowOffset: const Offset(0.8, 1.1), highlightOffset: const Offset(-0.65, -0.75), shadowColor: isDark ? const Color(0xC4000000) : AppColors.navEmbossShadow.withValues(alpha: 0.72), highlightColor: isDark ? Colors.white.withValues(alpha: 0.2) : Colors.white.withValues(alpha: 0.88), shadowBlur: isDark ? 2.6 : 1.9, highlightBlur: isDark ? 1.7 : 1.2, strokeWidth: 1.12, ); drawEmboss( radius: innerRadius, shadowOffset: const Offset(-0.4, -0.4), highlightOffset: const Offset(0.45, 0.55), shadowColor: isDark ? Colors.black.withValues(alpha: 0.58) : AppColors.navEmbossShadow.withValues(alpha: 0.58), highlightColor: isDark ? Colors.white.withValues(alpha: 0.14) : Colors.white.withValues(alpha: 0.76), shadowBlur: isDark ? 1.5 : 1.2, highlightBlur: isDark ? 1.1 : 0.9, strokeWidth: 0.96, ); final ringRect = Rect.fromCircle(center: center, radius: ringRadius); final metallic = SweepGradient( startAngle: rotation, endAngle: rotation + pi * 2, colors: const [ AppColors.navActiveGoldDeep, AppColors.navActiveGold, AppColors.navActiveGoldBright, AppColors.navActiveGoldPale, AppColors.navActiveGoldBright, AppColors.navActiveGoldDeep, ], stops: const [0.0, 0.16, 0.34, 0.5, 0.68, 1.0], ); final metallicPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2.8 ..shader = metallic.createShader(ringRect) ..isAntiAlias = true; canvas.drawCircle(center, ringRadius, metallicPaint); final chromaStrength = isDark ? 0.92 : 0.74; final chromaSweep = SweepGradient( startAngle: (rotation * 1.3) + 0.42, endAngle: (rotation * 1.3) + 0.42 + pi * 2, colors: [ Colors.transparent, Colors.transparent, AppColors.navActiveGold.withValues(alpha: 0.52 * chromaStrength), Colors.white.withValues(alpha: 0.92 * chromaStrength), AppColors.navActiveGoldPale.withValues(alpha: 0.66 * chromaStrength), Colors.transparent, Colors.transparent, AppColors.navActiveGold.withValues(alpha: 0.44 * chromaStrength), Colors.white.withValues(alpha: 0.84 * chromaStrength), AppColors.navActiveGoldPale.withValues(alpha: 0.6 * chromaStrength), Colors.transparent, Colors.transparent, ], stops: const [ 0.0, 0.09, 0.112, 0.126, 0.14, 0.175, 0.45, 0.468, 0.484, 0.5, 0.528, 1.0, ], ); final chromaPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.55 ..shader = chromaSweep.createShader(ringRect) ..blendMode = BlendMode.screen; canvas.drawCircle(center, ringRadius, chromaPaint); final ambient = Paint() ..style = PaintingStyle.stroke ..strokeWidth = isDark ? 1.0 : 0.86 ..color = isDark ? AppColors.navActiveGold.withValues(alpha: 0.2) : AppColors.navActiveGoldDeep.withValues(alpha: 0.16) ..maskFilter = MaskFilter.blur( BlurStyle.normal, isDark ? 2.8 : 1.3, ); canvas.drawCircle(center, ringRadius, ambient); final innerEdge = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 0.8 ..color = isDark ? Colors.white.withValues(alpha: 0.12) : const Color(0x330F172A); canvas.drawCircle(center, innerRadius, innerEdge); } @override bool shouldRepaint(covariant _MurattalGoldRingPainter oldDelegate) { return oldDelegate.progress != progress || oldDelegate.reducedMotion != reducedMotion || oldDelegate.isDark != isDark; } } /// Animated equalizer bar widget for the Murattal player. class _EqualizerBar extends StatefulWidget { final bool isPlaying; final int index; final Color color; const _EqualizerBar({ required this.isPlaying, required this.index, required this.color, }); @override State<_EqualizerBar> createState() => _EqualizerBarState(); } class _EqualizerBarState extends State<_EqualizerBar> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; // Each bar has a unique height range and speed for variety static const _barConfigs = [ [0.3, 0.9, 600], [0.2, 1.0, 500], [0.4, 0.8, 700], [0.1, 1.0, 450], [0.3, 0.9, 550], [0.2, 0.85, 650], [0.35, 0.95, 480], ]; @override void initState() { super.initState(); final config = _barConfigs[widget.index % _barConfigs.length]; final minHeight = config[0] as double; final maxHeight = config[1] as double; final durationMs = (config[2] as num).toInt(); _controller = AnimationController( vsync: this, duration: Duration(milliseconds: durationMs), ); _animation = Tween( begin: minHeight, end: maxHeight, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); if (widget.isPlaying) { _controller.repeat(reverse: true); } } @override void didUpdateWidget(covariant _EqualizerBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isPlaying && !oldWidget.isPlaying) { _controller.repeat(reverse: true); } else if (!widget.isPlaying && oldWidget.isPlaying) { _controller.animateTo(0.0, duration: const Duration(milliseconds: 300)); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( width: 6, height: 50 * _animation.value, decoration: BoxDecoration( color: widget.color.withValues(alpha: 0.6 + (_animation.value * 0.4)), borderRadius: BorderRadius.circular(3), ), ); }, ); } }