560 lines
23 KiB
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,
|
|
),
|
|
);
|
|
}
|
|
}
|