886 lines
33 KiB
Dart
886 lines
33 KiB
Dart
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/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<QuranMurattalScreen> createState() =>
|
|
_QuranMurattalScreenState();
|
|
}
|
|
|
|
class _QuranMurattalScreenState extends ConsumerState<QuranMurattalScreen> {
|
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
|
|
|
Map<String, dynamic>? _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<String, String>? _unsplashPhoto;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedQariId = widget.initialQariId ?? '05'; // Default to Misyari Rasyid Al-Afasi
|
|
_initDataAndPlayer();
|
|
_loadUnsplashPhoto();
|
|
}
|
|
|
|
Future<void> _loadUnsplashPhoto() async {
|
|
final photo = await UnsplashService.instance.getIslamicPhoto();
|
|
if (mounted && photo != null) {
|
|
setState(() => _unsplashPhoto = photo);
|
|
}
|
|
}
|
|
|
|
Future<void> _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<void> _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) {
|
|
final base = widget.isSimpleModeTab ? '/quran' : '/tools/quran';
|
|
context.pushReplacement(
|
|
'$base/$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),
|
|
...MuslimApiService.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<List<Map<String, dynamic>>>(
|
|
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
|
|
? Icon(LucideIcons.music, 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 isDark = Theme.of(context).brightness == Brightness.dark;
|
|
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(
|
|
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: _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(
|
|
MuslimApiService.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<double> _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<double>(
|
|
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),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|