didvan-app/lib/views/home/media/widgets/featured_podcast_card.dart

388 lines
13 KiB
Dart

// ignore_for_file: deprecated_member_use
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class FeaturedPodcastCard extends StatefulWidget {
final OverviewData podcast;
final VoidCallback onTap;
final Function(bool)? onPlayStateChanged;
const FeaturedPodcastCard({
super.key,
required this.podcast,
required this.onTap,
this.onPlayStateChanged,
});
@override
State<FeaturedPodcastCard> createState() => _FeaturedPodcastCardState();
}
class _FeaturedPodcastCardState extends State<FeaturedPodcastCard>
with WidgetsBindingObserver {
bool _isPlaying = false;
bool _isLoading = false;
double _progress = 0.0;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
// ignore: prefer_final_fields
var _subscriptions = <dynamic>[];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (widget.podcast.duration != null && widget.podcast.duration! > 0) {
_totalDuration = Duration(seconds: widget.podcast.duration!);
}
_checkCurrentPlayingState();
_setupAudioListener();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive) {
if (_isPlaying && MediaService.currentPodcast?.id == widget.podcast.id) {
MediaService.audioPlayer.pause();
}
}
}
void _checkCurrentPlayingState() {
if (MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_isPlaying = MediaService.audioPlayer.playing;
});
}
}
void _setupAudioListener() {
_subscriptions
.add(MediaService.audioPlayer.positionStream.listen((position) {
if (mounted && MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_currentPosition = position;
if (_totalDuration.inSeconds > 0) {
_progress = position.inSeconds / _totalDuration.inSeconds;
}
});
}
}));
_subscriptions
.add(MediaService.audioPlayer.durationStream.listen((duration) {
if (mounted &&
duration != null &&
MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_totalDuration = duration;
});
}
}));
_subscriptions.add(MediaService.audioPlayer.playingStream.listen((playing) {
if (mounted && MediaService.currentPodcast?.id == widget.podcast.id) {
setState(() {
_isPlaying = playing;
});
widget.onPlayStateChanged?.call(playing);
}
}));
}
Future<void> _togglePlayPause() async {
if (_isLoading) return;
try {
final isCurrentPodcast =
MediaService.currentPodcast?.id == widget.podcast.id;
if (!isCurrentPodcast) {
setState(() {
_isLoading = true;
});
if (widget.podcast.link == null || widget.podcast.link!.isEmpty) {
throw Exception('لینک پادکست معتبر نیست');
}
MediaService.currentPodcast = StudioDetailsData(
id: widget.podcast.id,
title: widget.podcast.title,
description: widget.podcast.description,
image: widget.podcast.image,
link: widget.podcast.link ?? '',
type: widget.podcast.type,
duration: widget.podcast.duration ?? 0,
iframe: widget.podcast.iframe,
createdAt: widget.podcast.createdAt,
order: 0,
marked: false,
comments: 0,
tags: [],
);
MediaService.isPlayingFromFeaturedCard = true;
await MediaService.handleAudioPlayback(
audioSource: widget.podcast.link!,
id: widget.podcast.id,
isVoiceMessage: false,
isNetworkAudio: true,
);
if (mounted) {
setState(() {
_isPlaying = true;
_isLoading = false;
});
widget.onPlayStateChanged?.call(true);
}
} else {
if (_isPlaying) {
await MediaService.audioPlayer.pause();
if (mounted) {
setState(() {
_isPlaying = false;
});
widget.onPlayStateChanged?.call(false);
}
} else {
await MediaService.audioPlayer.play();
if (mounted) {
setState(() {
_isPlaying = true;
});
widget.onPlayStateChanged?.call(true);
}
}
}
} catch (e) {
debugPrint('Error playing podcast: $e');
if (mounted) {
setState(() {
_isLoading = false;
_isPlaying = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در پخش پادکست: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
Future<void> _seekForward() async {
final newPosition = _currentPosition + const Duration(seconds: 10);
if (newPosition < _totalDuration) {
await MediaService.audioPlayer.seek(newPosition);
}
}
Future<void> _seekBackward() async {
final newPosition = _currentPosition - const Duration(seconds: 5);
if (newPosition > Duration.zero) {
await MediaService.audioPlayer.seek(newPosition);
} else {
await MediaService.audioPlayer.seek(Duration.zero);
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
@override
Widget build(BuildContext context) {
final bool isCurrentlyPlaying =
MediaService.currentPodcast?.id == widget.podcast.id;
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: DesignConfig.isDark
? const Color.fromARGB(255, 62, 62, 62)
: const Color.fromARGB(255, 235, 235, 235),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 2),
child: GestureDetector(
onTap: widget.onTap,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: SkeletonImage(
imageUrl: widget.podcast.image,
width: double.infinity,
height: 200,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DidvanText(
widget.podcast.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: DesignConfig.isDark ? Colors.white : Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 16),
Row(
children: [
SizedBox(
width: 45,
child: Text(
_formatDuration(_totalDuration),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.caption),
textAlign: TextAlign.left,
),
),
const SizedBox(width: 8),
Expanded(
child: AudioWaveformProgress(
progress: isCurrentlyPlaying
? _progress.clamp(0.0, 1.0)
: 0.0,
isActive: isCurrentlyPlaying,
onChanged: isCurrentlyPlaying
? (value) {
final newPosition = Duration(
seconds: (value * _totalDuration.inSeconds)
.toInt(),
);
MediaService.audioPlayer.seek(newPosition);
}
: null,
),
),
const SizedBox(width: 8),
SizedBox(
width: 45,
child: Text(
_formatDuration(_currentPosition),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.caption),
textAlign: TextAlign.right,
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: isCurrentlyPlaying ? _seekForward : null,
icon: SvgPicture.asset(
'lib/assets/icons/forward-10-seconds.svg',
width: 30,
height: 30,
color: DesignConfig.isDark
? const Color.fromARGB(255, 117, 117, 117)
: null),
),
const SizedBox(width: 15),
GestureDetector(
onTap: _togglePlayPause,
child: _isLoading
? const SizedBox(
width: 50,
height: 50,
child: Center(
child: CircularProgressIndicator(
color: Color(0xFF3B82F6),
strokeWidth: 3,
),
),
)
: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: SvgPicture.asset(
isCurrentlyPlaying && _isPlaying
? 'lib/assets/icons/pause-circle.svg'
: 'lib/assets/icons/play.svg',
key: ValueKey<bool>(
isCurrentlyPlaying && _isPlaying),
width:
isCurrentlyPlaying && _isPlaying ? 55 : 50,
height:
isCurrentlyPlaying && _isPlaying ? 55 : 50,
),
),
),
const SizedBox(width: 15),
IconButton(
onPressed: isCurrentlyPlaying ? _seekBackward : null,
icon: SvgPicture.asset(
'lib/assets/icons/backward-5-seconds.svg',
width: 30,
height: 30,
color: DesignConfig.isDark
? const Color.fromARGB(255, 117, 117, 117)
: null),
),
],
),
],
),
),
],
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
super.dispose();
}
}