import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../app/theme/app_colors.dart'; import '../../../data/services/equran_service.dart'; import '../../../data/services/unsplash_service.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../../../data/local/hive_boxes.dart'; import '../../../data/local/models/app_settings.dart'; /// Quran Murattal (audio player) screen. /// Implements full Surah playback using just_audio and EQuran v2 API. 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 { final AudioPlayer _audioPlayer = AudioPlayer(); 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; @override void initState() { super.initState(); _selectedQariId = widget.initialQariId ?? '05'; // Default to Misyari Rasyid Al-Afasi _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 EQuranService.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 { await _audioPlayer.setUrl(audioUrls[_selectedQariId]); 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(); _audioPlayer.dispose(); super.dispose(); } 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) { context.pushReplacement('/tools/quran/$surahNum/murattal?qariId=$_selectedQariId&autoplay=$autoplay'); } } void _showQariSelector() { showModalBottomSheet( context: context, 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), ...EQuranService.qariNames.entries.map((entry) { final isSelected = entry.key == _selectedQariId; return ListTile( leading: Icon( isSelected ? LucideIcons.checkCircle2 : LucideIcons.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, 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: EQuranService.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 ? Icon(LucideIcons.music, color: AppColors.primary, size: 20) : null, onTap: () { Navigator.pop(context); if (!isCurrentSurah) { context.pushReplacement( '/tools/quran/$surahNum/murattal?qariId=$_selectedQariId', ); } }, ); }, ); }, ), ), ], ); }, ); }, ); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final box = Hive.box(HiveBoxes.settings); final isSimpleMode = box.get('default')?.simpleMode ?? false; final surahName = _surahData?['namaLatin'] ?? 'Surah ${widget.surahId}'; final hasPhoto = _unsplashPhoto != null; return Scaffold( extendBodyBehindAppBar: hasPhoto, appBar: AppBar( leading: IconButton( icon: Icon(Icons.arrow_back, color: hasPhoto ? Colors.white : null), onPressed: () { if (widget.isSimpleModeTab) { context.go('/'); } else { context.pop(); } }, ), 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: 280, // leave room for the 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( EQuranService.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: _unsplashPhoto != null ? ImageFilter.blur(sigmaX: 20, sigmaY: 20) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Container( padding: const EdgeInsets.fromLTRB(24, 16, 24, 48), decoration: BoxDecoration( color: _unsplashPhoto != null ? Colors.white.withValues(alpha: 0.15) : (isDark ? AppColors.surfaceDark : AppColors.surfaceLight), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), border: _unsplashPhoto != null ? Border( top: BorderSide( color: Colors.white.withValues(alpha: 0.2), width: 0.5, ), ) : null, ), 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: Icon( LucideIcons.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: Icon( LucideIcons.skipBack, 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: Container( width: 64, height: 64, decoration: BoxDecoration( color: _unsplashPhoto != null ? Colors.white : AppColors.primary, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: (_unsplashPhoto != null ? Colors.white : AppColors.primary) .withValues(alpha: 0.3), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: _isBuffering ? Padding( padding: const EdgeInsets.all(18.0), child: CircularProgressIndicator( color: _unsplashPhoto != null ? Colors.black87 : Colors.white, strokeWidth: 3, ), ) : Icon( _isPlaying ? LucideIcons.pause : LucideIcons.play, size: 36, color: _unsplashPhoto != null ? Colors.black87 : AppColors.onPrimary, ), ), ), // Next Surah IconButton( onPressed: (int.tryParse(widget.surahId) ?? 1) < 114 ? () => _navigateToSurah(1) : null, icon: Icon( LucideIcons.skipForward, 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: Icon( LucideIcons.listMusic, 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: [ Icon(LucideIcons.user, size: 16, color: _unsplashPhoto != null ? Colors.white : AppColors.primary), const SizedBox(width: 8), Text( EQuranService.qariNames[_selectedQariId] ?? 'Ganti Qari', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _unsplashPhoto != null ? Colors.white : AppColors.primary, ), ), const SizedBox(width: 4), Icon(LucideIcons.chevronDown, size: 16, color: _unsplashPhoto != null ? Colors.white : AppColors.primary), ], ), ), ), ], ), ), ), ), ), // === ATTRIBUTION === if (_unsplashPhoto != null) Positioned( bottom: 280, left: 0, right: 0, child: GestureDetector( onTap: () { final url = _unsplashPhoto!['photographerUrl']; if (url != null && url.isNotEmpty) { launchUrl(Uri.parse('$url?utm_source=jamshalat_diary&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, ), ), ), ), ], ), ); } } /// 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), ), ); }, ); } }