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

395 lines
14 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:assets_audio_player/assets_audio_player.dart';
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/podcasts/podcasts_state.dart';
import 'package:didvan/views/home/widgets/audio/audio_slider.dart';
import 'package:didvan/views/home/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/item_title.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.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 Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
color: Theme.of(context).colorScheme.surface,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 20),
height: 3,
width: 50,
color: Theme.of(context).colorScheme.hint,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SkeletonImage(
imageUrl: podcast.image,
aspectRatio: 1 / 1,
),
),
const SizedBox(height: 16),
DidvanText(
podcast.title,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AudioSlider(
tag: 'podcast-${podcast.id}',
showTimer: true,
duration: podcast.duration,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Center(
child: StatefulBuilder(
builder: (context, setState) => Column(
children: [
DidvanIconButton(
icon: state.timer == null && !state.stopOnPodcastEnds
? DidvanIcons.sleep_timer_regular
: DidvanIcons.sleep_enabled_regular,
color: Theme.of(context).colorScheme.title,
onPressed: () => _showSleepTimer(
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: Column(
children: [
DidvanIconButton(
color: Theme.of(context).colorScheme.title,
size: 32,
icon: DidvanIcons.media_forward_solid,
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds: MediaService.audioPlayer.currentPosition
.value.inSeconds +
30,
),
);
},
),
DidvanText(
'30',
isEnglishFont: true,
color: Theme.of(context).colorScheme.title,
),
],
),
),
),
Expanded(
child: Center(
child: StreamBuilder<PlayingAudio?>(
stream: MediaService.audioPlayer.onReadyToPlay,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const CircularProgressIndicator();
}
return StreamBuilder<bool>(
stream: MediaService.audioPlayer.isPlaying,
builder: (context, snapshot) {
return _PlayPouseAnimatedIcon(
audioSource: podcast.link,
id: podcast.id,
);
},
);
},
),
),
),
Expanded(
child: Center(
child: Column(
children: [
DidvanIconButton(
size: 32,
icon: DidvanIcons.media_backward_solid,
color: Theme.of(context).colorScheme.title,
onPressed: () {
MediaService.audioPlayer.seek(
Duration(
seconds: max(
0,
MediaService.audioPlayer.currentPosition.value
.inSeconds -
10,
),
),
);
},
),
DidvanText(
'10',
isEnglishFont: true,
color: Theme.of(context).colorScheme.title,
),
],
),
),
),
Expanded(
child: Center(
child: BookmarkButton(
itemId: state.studio.id,
type: 'podcast',
gestureSize: 48,
color: Theme.of(context).colorScheme.title,
value: podcast.marked,
onMarkChanged: (value) => context
.read<PodcastsState>()
.changeMark(podcast.id, value, true),
),
),
),
],
),
],
),
);
}
Future<void> _showSleepTimer(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.showBottomSheet(
data: ActionSheetData(
content: StatefulBuilder(
builder: (context, setState) => Column(
children: [
const ItemTitle(
title: 'زمان خواب',
icon: DidvanIcons.sleep_timer_regular,
),
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.currentPosition.value.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;
const _PlayPouseAnimatedIcon(
{Key? key, required this.audioSource, required this.id})
: super(key: key);
@override
State<_PlayPouseAnimatedIcon> createState() => __PlayPouseAnimatedIconState();
}
class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
@override
void didUpdateWidget(covariant _PlayPouseAnimatedIcon oldWidget) {
_handleAnimation();
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: DesignConfig.lowAnimationDuration,
);
}
void _handleAnimation() {
if (MediaService.audioPlayer.isPlaying.value) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
return InkWrapper(
borderRadius: BorderRadius.circular(100),
onPressed: () {
MediaService.handleAudioPlayback(
audioSource: widget.audioSource,
isVoiceMessage: false,
id: widget.id,
);
_handleAnimation();
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.title,
shape: BoxShape.circle,
),
child: AnimatedIcon(
size: 40,
color: Theme.of(context).colorScheme.surface,
icon: AnimatedIcons.play_pause,
progress: _animationController,
),
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}