didvan-app/lib/views/widgets/audio/audio_player_widget.dart

560 lines
23 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:just_audio/just_audio.dart';
import 'package:provider/provider.dart';
class AudioPlayerWidget extends StatelessWidget {
final StudioDetailsData podcast;
const AudioPlayerWidget({Key? key, required this.podcast}) : super(key: key);
@override
Widget build(BuildContext context) {
final state = context.read<StudioDetailsState>();
return Stack(
children: [
SkeletonImage(
imageUrl: podcast.image,
aspectRatio: 1 / 1.3,
borderRadius: BorderRadius.circular(0),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius.vertical(top: Radius.circular(24.0)),
color: Theme.of(context).colorScheme.surface,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 30,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
StreamBuilder<double>(
stream: MediaService.audioPlayer.speedStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
return const Column(
children: [
SizedBox(),
// PopupMenuButton(
// child: Container(
// width: 46,
// alignment: Alignment.center,
// margin: const EdgeInsets.fromLTRB(12, 0, 0, 0), // تغییر: 46 پایین حذف شد
// padding: const EdgeInsets.only(top: 2),
// decoration: BoxDecoration(
// borderRadius: DesignConfig.mediumBorderRadius,
// border: Border.all(
// color:
// Theme.of(context).colorScheme.title)),
// child: DidvanText(
// '${snapshot.data!.toString().replaceAll('.0', '')}X'),
// ),
// onSelected: (value) async {
// await MediaService.audioPlayer.setSpeed(value);
// },
// itemBuilder: (BuildContext context) =>
// <PopupMenuEntry>[
// popUpSpeed(value: 0.5),
// popUpSpeed(value: 0.75),
// popUpSpeed(value: 1.0),
// popUpSpeed(value: 1.25),
// popUpSpeed(value: 1.5),
// popUpSpeed(value: 2.0),
// ],
// ),
],
);
}),
Expanded(
child: StreamBuilder<Duration>(
stream: MediaService.audioPlayer.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return StreamBuilder<Duration?>(
stream: MediaService.audioPlayer.durationStream,
builder: (context, durationSnapshot) {
final totalDuration = durationSnapshot.data ??
Duration(milliseconds: podcast.duration);
final progress =
totalDuration.inMilliseconds > 0
? position.inMilliseconds /
totalDuration.inMilliseconds
: 0.0;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
_formatDuration(totalDuration),
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.title
.withOpacity(0.7),
),
),
const SizedBox(width: 8),
Expanded(
child: AudioWaveformProgress(
progress: progress.clamp(0.0, 1.0),
isActive: true,
onChanged: (value) {
final newPosition = Duration(
milliseconds:
(totalDuration.inMilliseconds *
value)
.round(),
);
MediaService.audioPlayer
.seek(newPosition);
},
),
),
const SizedBox(width: 8),
Text(
_formatDuration(position),
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.title
.withOpacity(0.7),
),
),
],
);
},
);
},
),
),
],
),
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Center(
child: StatefulBuilder(
builder: (context, setState) => Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 20, 8),
child: Center(
child: IconButton(
icon: SvgPicture.asset(
'lib/assets/icons/timer-pause.svg',
width: 35,
height: 35,
colorFilter: const ColorFilter.mode(
Color.fromARGB(255, 102, 102, 102),
BlendMode.srcIn,
),
),
onPressed: () => _showSleepTimer(
context,
state,
() => setState(() {}),
),
),
),
),
// DidvanIconButton(
// icon: state.timer == null &&
// !state.stopOnPodcastEnds
// ? DidvanIcons.sleep_timer_regular
// : DidvanIcons.sleep_enabled_regular,
// color: Theme.of(context).colorScheme.title,
// onPressed: () => _showSleepTimer(
// context,
// state,
// () => setState(() {}),
// ),
// ),
// if (state.timer != null)
// DidvanText(
// state.stopOnPodcastEnds
// ? 'پایان پادکست'
// : '\'${state.timerValue}',
// isEnglishFont: true,
// style: Theme.of(context).textTheme.labelSmall,
// color: Theme.of(context).colorScheme.title,
// ),
],
),
),
),
),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds: max(
0,
MediaService
.audioPlayer.position.inSeconds +
10,
),
),
);
},
icon: SvgPicture.asset(
'lib/assets/icons/forward-10-seconds.svg'),
),
),
),
),
Expanded(
child: Center(
child: StreamBuilder<PlayerState>(
stream: MediaService.audioPlayer.playerStateStream,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const CircularProgressIndicator();
}
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.playingStream,
builder: (context, snapshot) {
final isPlaying = snapshot.data ?? false;
return _PlayPouseAnimatedIcon(
audioSource: podcast.link,
id: podcast.id,
isPlaying: isPlaying,
);
},
);
},
),
),
),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds: max(
0,
MediaService
.audioPlayer.position.inSeconds -
5,
),
),
);
},
icon: SvgPicture.asset(
'lib/assets/icons/backward-5-seconds.svg'),
),
),
),
),
Expanded(
child: StreamBuilder<double>(
stream: MediaService.audioPlayer.speedStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
return Center(
child: PopupMenuButton(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Container(
width: 46,
alignment: Alignment.center,
margin: const EdgeInsets.fromLTRB(
12, 0, 0, 0), // تغییر: 46 پایین حذف شد
padding: const EdgeInsets.only(top: 2),
decoration: BoxDecoration(
borderRadius:
DesignConfig.mediumBorderRadius,
border: Border.all(
color: const Color.fromARGB(
255, 102, 102, 102))),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: DidvanText(
'${snapshot.data!.toString().replaceAll('.0', '')}X',
style: const TextStyle(
fontWeight: FontWeight.w900,
color: Color.fromARGB(
255, 102, 102, 102)),
),
),
),
),
onSelected: (value) async {
await MediaService.audioPlayer.setSpeed(value);
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry>[
popUpSpeed(value: 0.5),
popUpSpeed(value: 0.75),
popUpSpeed(value: 1.0),
popUpSpeed(value: 1.25),
popUpSpeed(value: 1.5),
popUpSpeed(value: 2.0),
],
),
);
},
),
),
],
),
// اضافه کردن کمی فاصله در پایین
const SizedBox(height: 16),
],
),
),
),
],
);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '$hours:${twoDigits(minutes)}:${twoDigits(seconds)}';
} else {
return '${twoDigits(minutes)}:${twoDigits(seconds)}';
}
}
PopupMenuItem<dynamic> popUpSpeed({required double value}) {
return PopupMenuItem(
value: value,
height: 32,
child: DidvanText(
'${value}X',
),
);
}
Future<void> _showSleepTimer(
BuildContext context, StudioDetailsState state, update) async {
int timerValue = 10;
final controller = FixedExtentScrollController();
bool isInit = true;
Future.delayed(
const Duration(milliseconds: 100),
() async {
await controller.animateTo(
state.timerValue * 10,
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
);
isInit = false;
},
);
await ActionSheetUtils(context).showBottomSheet(
data: ActionSheetData(
content: StatefulBuilder(
builder: (context, setState) => Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
'lib/assets/icons/timer-pause.svg',
height: 24,
color: const Color.fromARGB(255, 102, 102, 102),
),
const SizedBox(
width: 10,
),
const Text(
'زمان خواب',
style: TextStyle(color: Color.fromARGB(255, 102, 102, 102)),
)
],
),
const SizedBox(height: 24),
DidvanText(
'$timerValue دقیقه',
style: Theme.of(context).textTheme.displaySmall,
),
const SizedBox(height: 12),
const Icon(DidvanIcons.caret_down_solid),
const SizedBox(height: 8),
SizedBox(
height: 50,
child: RotatedBox(
quarterTurns: 3,
child: ListWheelScrollView(
physics: const FixedExtentScrollPhysics(),
controller: controller,
itemExtent: 10,
onSelectedItemChanged: (index) {
if (!isInit) {
state.stopOnPodcastEnds = false;
}
final minutes = index == 0 ? 1 : index;
timerValue = minutes;
setState(() {});
},
children: [
for (var i = 0; i < 61; i++) ...[
if (i % 5 == 0)
Center(
child: Container(
color: Theme.of(context).colorScheme.text,
width: 50,
height: 3,
),
),
if (i % 5 != 0) const SizedBox(height: 3),
],
],
),
),
),
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 64),
child: DidvanButton(
style: state.timerValue == MediaService.duration?.inMinutes &&
state.stopOnPodcastEnds
? ButtonStyleMode.primary
: ButtonStyleMode.flat,
title: 'پایان پادکست',
onPressed: () async {
state.timerValue = MediaService.duration!.inMinutes -
MediaService.audioPlayer.position.inMinutes;
await controller.animateTo(
state.timerValue * 10,
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
);
state.stopOnPodcastEnds = true;
setState(() {});
},
),
),
],
),
),
onConfirmed: () {
if (!state.stopOnPodcastEnds) {
state.timer = Timer.periodic(
const Duration(minutes: 1),
(timer) {
timerValue--;
if (timerValue == 0) {
MediaService.audioPlayer.pause();
state.stopOnPodcastEnds = false;
state.timer?.cancel();
state.timer = null;
state.timerValue = 10;
state.update();
}
},
);
}
state.timerValue = timerValue;
update();
},
confrimTitle: 'شروع زمان خواب',
dismissTitle: 'لغو',
onDismissed: () {
state.timer?.cancel();
state.timer = null;
state.timerValue = 10;
update();
},
),
);
controller.dispose();
}
}
class _PlayPouseAnimatedIcon extends StatefulWidget {
final String audioSource;
final int id;
final bool isPlaying;
const _PlayPouseAnimatedIcon(
{Key? key,
required this.audioSource,
required this.id,
required this.isPlaying})
: super(key: key);
@override
State<_PlayPouseAnimatedIcon> createState() => __PlayPouseAnimatedIconState();
}
class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> {
@override
Widget build(BuildContext context) {
return InkWrapper(
borderRadius: BorderRadius.circular(100),
onPressed: () {
MediaService.handleAudioPlayback(
audioSource: widget.audioSource,
isVoiceMessage: false,
id: widget.id,
);
},
child: SvgPicture.asset(
widget.isPlaying
? 'lib/assets/icons/pause-circle.svg'
: 'lib/assets/icons/video-circle.svg',
width: 65,
height: 65,
),
);
}
}