388 lines
13 KiB
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();
|
|
}
|
|
}
|