diff --git a/lib/main.dart b/lib/main.dart index 8557d68..ae033f4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -123,6 +123,9 @@ class _DidvanState extends State with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); // MediaService.audioPlayer.dispose(); + if(MediaService.currentPodcast != null){ + MediaService.audioPlayer.dispose(); + } super.dispose(); } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index ad24918..7bf54f7 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -48,6 +48,8 @@ import 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart' if (dart.library.html) 'package:didvan/views/podcasts/studio_details/studio_details.web.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/widgets/audio/player_navbar.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart'; diff --git a/lib/services/media/audio_handler.dart b/lib/services/media/audio_handler.dart new file mode 100644 index 0000000..6a582c0 --- /dev/null +++ b/lib/services/media/audio_handler.dart @@ -0,0 +1,133 @@ +import 'dart:async'; + +import 'package:audioplayers/audioplayers.dart'; + + +class AudioHandler { + late AudioPlayer _player; + PlayerState? _playerState; + Duration? audioDuration = Duration.zero; + Duration? audioPosition = Duration.zero; + bool isLoading = true; + + StreamSubscription? durationSubscriptionS; + StreamSubscription? positionSubscriptionS; + StreamSubscription? playerCompleteSubscriptionS; + StreamSubscription? playerStateChangeSubscriptionS; + + AudioPlayer get player => _player; + + PlayerState? get playerState => _playerState; + + bool get isPlaying => _playerState == PlayerState.playing; + + bool get isPaused => _playerState == PlayerState.paused; + + String get durationText => audioDuration?.toString().split('.').first ?? ''; + + String get positionText => audioPosition?.toString().split('.').first ?? ''; + + AudioHandler(String url) { + _player = AudioPlayer(); + _player.setReleaseMode(ReleaseMode.stop); + _player + .play(UrlSource(url)) + .then((value) { + _player.pause(); + isLoading = false; + _playerState = PlayerState.paused; + }); + player.getDuration().then((value) => audioDuration = value); + player.getCurrentPosition().then((value) => audioPosition = value); + } + + StreamSubscription? durationSubscription(Function(Duration d) func) { + return durationSubscriptionS = player.onDurationChanged.listen((duration) { + // setState(() => _duration = duration); + audioDuration = duration; + func(duration); + }); + } + + StreamSubscription? positionSubscription(Function(Duration d) func) { + return positionSubscriptionS = player.onPositionChanged.listen( + (p) { + audioPosition = p; + func(p); + }, + ); + } + + StreamSubscription? playerCompleteSubscription(Function() func) { + return playerCompleteSubscriptionS = + player.onPlayerComplete.listen((event) { + stop(); + func(); + }); + } + + StreamSubscription? playerStateChangeSubscription( + Function(PlayerState) func) { + return playerStateChangeSubscriptionS = + player.onPlayerStateChanged.listen((state) { + _playerState = state; + func(state); + }); + } + + Future play() async { + await _player.resume(); + // _playerState = PlayerState.playing; + } + + Future pause() async { + await _player.pause(); + // _playerState = PlayerState.paused; + } + + Future stop() async { + await _player.stop(); + // _playerState = PlayerState.stopped; + audioPosition = Duration.zero; + } + +// void _initStreams() { +// durationSubscription = player.onDurationChanged.listen((duration) { +// setState(() => _duration = duration); +// }); +// +// _positionSubscription = player.onPositionChanged.listen( +// (p) { +// setState(() { +// _position = p; +// for (var i = 0; i < bars.length / 10; i++) { +// if (i <= _position!.inSeconds) { +// bars[i].color = primaryColor; +// } +// } +// }); +// }, +// ); +// +// _playerCompleteSubscription = player.onPlayerComplete.listen((event) { +// setState(() { +// _playerState = PlayerState.stopped; +// _position = Duration.zero; +// }); +// }); +// +// _playerCompleteSubscription = player.onSeekComplete.listen((event) { +// setState(() { +// print( +// "Complete------------------------------------------------------------"); +// }); +// }); +// +// _playerStateChangeSubscription = +// player.onPlayerStateChanged.listen((state) { +// setState(() { +// _playerState = state; +// }); +// }); +// } +} diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 1b18fd2..423f14b 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -24,70 +24,70 @@ class MediaService { bool isVoiceMessage = true, void Function(bool isNext)? onTrackChanged, }) async { - String tag; - tag = - '${currentPodcast?.description == 'radar' ? 'radar' : isVoiceMessage ? 'message' : 'podcast'}-$id'; - if (!isVoiceMessage && MediaProvider.downloadedItemIds.contains(id)) { - audioSource = '${StorageService.appDocsDir}/podcasts/podcast-$id.mp3'; - isNetworkAudio = false; - } - if (audioPlayerTag == tag) { - await audioPlayer.playOrPause(); - return; - } - await audioPlayer.stop(); - audioPlayerTag = tag; - Audio audio; - String source; - if (isNetworkAudio) { - if (isVoiceMessage) { - source = - '${RequestHelper.baseUrl + audioSource}?accessToken=${RequestService.token}'; - } else { - source = audioSource; + try { + String tag; + tag = + '${currentPodcast?.description == 'radar' ? 'radar' : isVoiceMessage ? 'message' : 'podcast'}-$id'; + if (!isVoiceMessage && MediaProvider.downloadedItemIds.contains(id)) { + audioSource = '${StorageService.appDocsDir}/podcasts/podcast-$id.mp3'; + isNetworkAudio = false; } - audio = Audio.network( - kIsWeb ? source.replaceAll('%3A', ':') : source, - metas: isVoiceMessage - ? null - : Metas( - artist: 'استودیو دیدوان', - title: currentPodcast?.title ?? '', - ), - ); - } else { - audio = Audio.file( - audioSource, - metas: isVoiceMessage - ? null - : Metas( - artist: 'استودیو دیدوان', - title: currentPodcast?.title ?? '', - ), + if (audioPlayerTag == tag) { + await audioPlayer.playOrPause(); + + return; + } + // await audioPlayer.stop(); + audioPlayerTag = tag; + Audio audio; + String source; + if (isNetworkAudio) { + if (isVoiceMessage) { + source = + '${RequestHelper.baseUrl + audioSource}?accessToken=${RequestService.token}'; + } else { + source = audioSource; + } + audio = Audio.network( + kIsWeb ? source.replaceAll('%3A', ':') : source, + metas: isVoiceMessage + ? null + : Metas( + artist: 'استودیو دیدوان', + title: currentPodcast?.title ?? '', + ), + ); + } else { + audio = Audio.file( + audioSource, + metas: isVoiceMessage + ? null + : Metas( + artist: 'استودیو دیدوان', + title: currentPodcast?.title ?? '', + ), + ); + } + await audioPlayer.open( + audio, + showNotification: !isVoiceMessage, + notificationSettings: NotificationSettings( + customStopAction: (_) => resetAudioPlayer(), + customNextAction: (_) => onTrackChanged?.call(true), + customPrevAction: (_) => onTrackChanged?.call(false), + ), ); + } catch (e) { + // resetAudioPlayer(); + rethrow; } - await audioPlayer.open( - audio, - showNotification: !isVoiceMessage, - notificationSettings: NotificationSettings( - customStopAction: (_) => resetAudioPlayer(), - customNextAction: (_) => onTrackChanged?.call(true), - customPrevAction: (_) => onTrackChanged?.call(false), - ), - ); } static Future resetAudioPlayer() async { audioPlayerTag = null; currentPodcast = null; podcastPlaylistArgs = null; - if(MediaService.audioPlayer.isPlaying.value){ - MediaService.audioPlayer.stop(); - - }else{ - MediaService.audioPlayer.dispose(); - - } + await MediaService.audioPlayer.stop(); } static Future pickImage({required ImageSource source}) async { diff --git a/lib/views/podcasts/studio_details/studio_details_state.dart b/lib/views/podcasts/studio_details/studio_details_state.dart index b8e4ccd..ba73964 100644 --- a/lib/views/podcasts/studio_details/studio_details_state.dart +++ b/lib/views/podcasts/studio_details/studio_details_state.dart @@ -123,7 +123,9 @@ class StudioDetailsState extends CoreProvier { notifyListeners(); } } catch (e) { - MediaService.audioPlayer.dispose(); + // MediaService.resetAudioPlayer(); + update(); + appState = AppState.idle; rethrow; } } @@ -163,7 +165,7 @@ class StudioDetailsState extends CoreProvier { } } else { MediaService.audioPlayer.pause(); - MediaService.audioPlayer.dispose(); + // MediaService.audioPlayer.dispose(); } } diff --git a/lib/views/widgets/audio/player_navbar.dart b/lib/views/widgets/audio/player_navbar.dart new file mode 100644 index 0000000..6a613a5 --- /dev/null +++ b/lib/views/widgets/audio/player_navbar.dart @@ -0,0 +1,338 @@ +import 'package:assets_audio_player/assets_audio_player.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:provider/provider.dart'; + +import '../../../config/design_config.dart'; +import '../../../constants/app_icons.dart'; +import '../../../models/enums.dart'; +import '../../../models/requests/radar.dart'; +import '../../../routes/routes.dart'; +import '../../../services/media/media.dart'; +import '../../../utils/action_sheet.dart'; +import '../../podcasts/podcasts_state.dart'; +import '../../podcasts/studio_details/studio_details_state.dart'; +import '../../podcasts/studio_details/widgets/studio_details_widget.dart'; +import '../didvan/icon_button.dart'; +import '../didvan/text.dart'; +import '../skeleton_image.dart'; +import 'audio_player_widget.dart'; +import 'audio_slider.dart'; + +class PlayerNavBar extends StatefulWidget { + final bool inHome; + + const PlayerNavBar({Key? key, required this.inHome}) : super(key: key); + + @override + State createState() => _PlayerNavBarState(); +} + +class _PlayerNavBarState extends State { + bool _enablePlayerController(StudioDetailsState state) => + MediaService.currentPodcast != null || + (MediaService.audioPlayerTag?.contains('podcast') ?? false); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: MediaService.audioPlayer.isPlaying, + builder: (context, isPlaying) => GestureDetector( + onTap: () => (MediaService.currentPodcast == null && + (MediaService.audioPlayerTag ?? '') + .split('-')[1] + .isNotEmpty) || + MediaService.currentPodcast?.description == 'radar' + ? Navigator.of(context).pushNamed( + Routes.radarDetails, + arguments: { + 'onMarkChanged': (id, value) {}, + 'onCommentsChanged': (id, value) {}, + 'id': MediaService.currentPodcast?.id, + 'args': const RadarRequestArgs(page: 0), + 'hasUnmarkConfirmation': false, + }, + ) + : (MediaService.audioPlayerTag ?? '').split('-')[1].isNotEmpty + ? _showPlayerBottomSheet(context) + : null, + child: Consumer( + builder: (context, state, child) => AnimatedContainer( + height: widget.inHome + ? _enablePlayerController(state) + ? 72 + 32 + 32 + : 72 + : null, + duration: DesignConfig.lowAnimationDuration, + decoration: BoxDecoration( + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.navigation, + borderRadius: BorderRadius.vertical( + top: const Radius.circular(16), + bottom: + widget.inHome ? Radius.zero : const Radius.circular(16)), + ), + alignment: widget.inHome ? Alignment.topCenter : Alignment.center, + child: Builder(builder: (context) { + if (!_enablePlayerController(state)) { + return const SizedBox(); + } + if (state.appState == AppState.failed) { + Future.delayed(const Duration(seconds: 2), () { + MediaService.resetAudioPlayer(); + state.update(); + // _enablePlayerController(state); + }); + return Padding( + padding: const EdgeInsets.only(top: 18.0), + child: DidvanText( + 'اتصال اینترنت برقرار نمی‌باشد', + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.secondCTA, + ), + ); + } + if (MediaService.currentPodcast == null) { + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 16) + .copyWith(bottom: widget.inHome ? 72 + 16 : 16), + child: Row( + children: [ + DidvanIconButton( + icon: DidvanIcons.close_regular, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + gestureSize: 28, + onPressed: () async { + await MediaService.resetAudioPlayer(); + }, + ), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.only(left: 48), + child: SpinKitThreeBounce( + size: 18, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.secondCTA, + ), + ), + ), + ), + ], + ), + ); + } + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanIconButton( + icon: DidvanIcons.close_regular, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + gestureSize: 28, + onPressed: () async { + await MediaService.resetAudioPlayer(); + // _enablePlayerController(state); + // state.update(); + }, + ), + const SizedBox(width: 8), + SkeletonImage( + imageUrl: MediaService.currentPodcast!.image, + width: 32, + height: 32, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + MediaService.currentPodcast!.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + ), + AudioSlider( + disableThumb: true, + tag: MediaService.audioPlayerTag!, + ), + ], + ), + ), + const SizedBox( + width: 8, + ), + // StreamBuilder( + // stream: MediaService.audioPlayer.onReadyToPlay, + // builder: (context, snapshot) { + // if (snapshot.data == null || + // state.appState == AppState.busy && + // MediaService.currentPodcast?.description != + // 'radar') { + // return SizedBox( + // height: 18, + // width: 18, + // child: CircularProgressIndicator( + // strokeWidth: 2, + // color: DesignConfig.isDark + // ? Theme.of(context).colorScheme.title + // : Theme.of(context).colorScheme.secondCTA, + // ), + // ); + // } + // return const SizedBox(); + // }, + // ), + + (state.appState != AppState.busy && + isPlaying.data != null || + MediaService.currentPodcast?.description == 'radar') + ? DidvanIconButton( + gestureSize: 28, + color: DesignConfig.isDark + ? null + : Theme.of(context).colorScheme.secondCTA, + icon: isPlaying.data! + ? DidvanIcons.pause_solid + : DidvanIcons.play_solid, + onPressed: () { + if (state.args?.type == 'video') { + state.getStudioDetails( + MediaService.currentPodcast!.id, + args: state.podcastArgs, + fetchOnly: true, + ); + } + MediaService.handleAudioPlayback( + audioSource: MediaService.currentPodcast!.link, + id: MediaService.currentPodcast!.id, + isVoiceMessage: false, + ); + }, + ) + : SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.secondCTA, + ), + ) + ], + ), + ); + }), + ), + ), + ), + ); + } + + void _showPlayerBottomSheet(BuildContext context) { + final sheetKey = GlobalKey(); + bool isExpanded = false; + final detailsState = context.read(); + if (detailsState.args?.type == 'video') { + detailsState.getStudioDetails( + MediaService.currentPodcast!.id, + args: detailsState.podcastArgs, + fetchOnly: true, + ); + } + final state = context.read(); + showModalBottomSheet( + constraints: BoxConstraints( + maxWidth: ActionSheetUtils.mediaQueryData.size.width, + ), + backgroundColor: Colors.transparent, + context: context, + isScrollControlled: true, + builder: (context) => ChangeNotifierProvider.value( + value: state, + child: Consumer( + builder: (context, state, child) => MediaQuery( + data: ActionSheetUtils.mediaQueryData, + child: ExpandableBottomSheet( + key: sheetKey, + background: Align( + alignment: Alignment.bottomCenter, + child: Container( + height: MediaQuery.of(context).size.height * 0.7, + color: Theme.of(context).colorScheme.surface, + ), + ), + persistentHeader: GestureDetector( + onVerticalDragUpdate: (details) { + if (details.delta.dy > 10) { + Navigator.of(context).pop(); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AudioPlayerWidget( + podcast: MediaService.currentPodcast!, + ), + Container( + width: MediaQuery.of(context).size.width, + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.angle_up_regular, + onPressed: () { + if (!isExpanded) { + sheetKey.currentState?.expand(); + isExpanded = true; + } else { + isExpanded = false; + sheetKey.currentState?.contract(); + } + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + ), + expandableContent: state.appState == AppState.busy + ? Container( + height: MediaQuery.of(context).size.height / 2, + alignment: Alignment.center, + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ) + : StudioDetailsWidget( + onMarkChanged: (id, value) => context + .read() + .changeMark(id, value, true), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/didvan/app_bar.dart b/lib/views/widgets/didvan/app_bar.dart index 163768f..2966f72 100644 --- a/lib/views/widgets/didvan/app_bar.dart +++ b/lib/views/widgets/didvan/app_bar.dart @@ -36,13 +36,17 @@ class DidvanAppBar extends StatelessWidget implements PreferredSizeWidget { ), child: Row( children: [ - IconButton( - onPressed: () => Navigator.of(context).pop(), - color: Theme.of(context).colorScheme.title, - icon: const Icon( - DidvanIcons.back_regular, - ), - ), + appBarData.hasBack? Column( + children: [ + IconButton( + onPressed: () => Navigator.of(context).pop(), + color: Theme.of(context).colorScheme.title, + icon: const Icon( + DidvanIcons.back_regular, + ), + ), + ], + ):const SizedBox(), const SizedBox(width: 16), Expanded( child: Align( diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart index dd773be..43da2d1 100644 --- a/lib/views/widgets/didvan/bnb.dart +++ b/lib/views/widgets/didvan/bnb.dart @@ -16,10 +16,14 @@ import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:provider/provider.dart'; +import '../audio/player_navbar.dart'; + class DidvanBNB extends StatelessWidget { final int currentTabIndex; final void Function(int index) onTabChanged; @@ -32,7 +36,7 @@ class DidvanBNB extends StatelessWidget { Widget build(BuildContext context) { return Stack( children: [ - const _PlayerNavBar(), + const PlayerNavBar(inHome: true,), Positioned( bottom: 0, left: 0, @@ -92,310 +96,6 @@ class DidvanBNB extends StatelessWidget { } } -class _PlayerNavBar extends StatelessWidget { - const _PlayerNavBar({Key? key}) : super(key: key); - - bool _enablePlayerController(StudioDetailsState state) => - MediaService.currentPodcast != null || - (MediaService.audioPlayerTag?.contains('podcast') ?? false); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: MediaService.audioPlayer.isPlaying, - builder: (context, snapshot) => GestureDetector( - onTap: () => (MediaService.currentPodcast == null && - (MediaService.audioPlayerTag ?? '') - .split('-')[1] - .isNotEmpty) || - MediaService.currentPodcast?.description == 'radar' - ? Navigator.of(context).pushNamed( - Routes.radarDetails, - arguments: { - 'onMarkChanged': (id, value) {}, - 'onCommentsChanged': (id, value) {}, - 'id': MediaService.currentPodcast?.id, - 'args': const RadarRequestArgs(page: 0), - 'hasUnmarkConfirmation': false, - }, - ) - : (MediaService.audioPlayerTag ?? '').split('-')[1].isNotEmpty - ? _showPlayerBottomSheet(context) - : null, - child: Consumer( - builder: (context, state, child) => AnimatedContainer( - padding: const EdgeInsets.only(top: 12), - duration: DesignConfig.lowAnimationDuration, - height: _enablePlayerController(state) ? 128 : 72, - decoration: BoxDecoration( - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.focused - : Theme.of(context).colorScheme.navigation, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), - ), - ), - alignment: Alignment.topCenter, - child: Builder(builder: (context) { - if (!_enablePlayerController(state)) return const SizedBox(); - if (state.appState == AppState.failed) { - Future.delayed(const Duration(seconds: 2), () async{ - await MediaService.resetAudioPlayer(); - _enablePlayerController(state); - }); - return DidvanText( - 'اتصال اینترنت برقرار نمی‌باشد', - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title - : Theme.of(context).colorScheme.secondCTA, - ); - } - if (MediaService.currentPodcast == null) { - return SizedBox( - height: 32, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 16, - ), - child: DidvanIconButton( - icon: DidvanIcons.close_regular, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - gestureSize: 28, - onPressed: () { - MediaService.resetAudioPlayer(); - state.update(); - }, - ), - ), - Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.only(left: 48), - child: SpinKitThreeBounce( - size: 18, - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title - : Theme.of(context).colorScheme.secondCTA, - ), - ), - ), - ), - ], - ), - ); - } - return SizedBox( - height: 56, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 16, - ), - child: DidvanIconButton( - icon: DidvanIcons.close_regular, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - gestureSize: 28, - onPressed: () async { - await MediaService.resetAudioPlayer(); - _enablePlayerController(state); - }, - ), - ), - SkeletonImage( - imageUrl: MediaService.currentPodcast!.image, - width: 32, - height: 32, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DidvanText( - MediaService.currentPodcast!.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - ), - AudioSlider( - disableThumb: true, - tag: MediaService.audioPlayerTag!, - ), - ], - ), - ), - StreamBuilder( - stream: MediaService.audioPlayer.onReadyToPlay, - builder: (context, snapshot) { - if (snapshot.data == null || - state.appState == AppState.busy && - MediaService.currentPodcast?.description != - 'radar') { - return Padding( - padding: const EdgeInsets.only( - top: 4, - left: 16, - right: 16, - ), - child: SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title - : Theme.of(context).colorScheme.secondCTA, - ), - ), - ); - } - return const SizedBox(); - }, - ), - if (state.appState != AppState.busy && - snapshot.data != null || - MediaService.currentPodcast?.description == 'radar') - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 16, - ), - child: DidvanIconButton( - gestureSize: 28, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - icon: snapshot.data! - ? DidvanIcons.pause_solid - : DidvanIcons.play_solid, - onPressed: () { - if (state.args?.type == 'video') { - state.getStudioDetails( - MediaService.currentPodcast!.id, - args: state.podcastArgs, - fetchOnly: true, - ); - } - MediaService.handleAudioPlayback( - audioSource: MediaService.currentPodcast!.link, - id: MediaService.currentPodcast!.id, - isVoiceMessage: false, - ); - }, - ), - ), - ], - ), - ); - }), - ), - ), - ), - ); - } - - void _showPlayerBottomSheet(BuildContext context) { - final sheetKey = GlobalKey(); - bool isExpanded = false; - final detailsState = context.read(); - if (detailsState.args?.type == 'video') { - detailsState.getStudioDetails( - MediaService.currentPodcast!.id, - args: detailsState.podcastArgs, - fetchOnly: true, - ); - } - final state = context.read(); - showModalBottomSheet( - constraints: BoxConstraints( - maxWidth: ActionSheetUtils.mediaQueryData.size.width, - ), - backgroundColor: Colors.transparent, - context: context, - isScrollControlled: true, - builder: (context) => ChangeNotifierProvider.value( - value: state, - child: Consumer( - builder: (context, state, child) => MediaQuery( - data: ActionSheetUtils.mediaQueryData, - child: ExpandableBottomSheet( - key: sheetKey, - background: Align( - alignment: Alignment.bottomCenter, - child: Container( - height: MediaQuery.of(context).size.height * 0.7, - color: Theme.of(context).colorScheme.surface, - ), - ), - persistentHeader: GestureDetector( - onVerticalDragUpdate: (details) { - if (details.delta.dy > 10) { - Navigator.of(context).pop(); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AudioPlayerWidget( - podcast: MediaService.currentPodcast!, - ), - Container( - width: MediaQuery.of(context).size.width, - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - DidvanIconButton( - size: 32, - icon: DidvanIcons.angle_up_regular, - onPressed: () { - if (!isExpanded) { - sheetKey.currentState?.expand(); - isExpanded = true; - } else { - isExpanded = false; - sheetKey.currentState?.contract(); - } - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ], - ), - ), - expandableContent: state.appState == AppState.busy - ? Container( - height: MediaQuery.of(context).size.height / 2, - alignment: Alignment.center, - child: SpinKitSpinningLines( - color: Theme.of(context).colorScheme.primary, - ), - ) - : StudioDetailsWidget( - onMarkChanged: (id, value) => context - .read() - .changeMark(id, value, true), - ), - ), - ), - ), - ), - ); - } -} class _NavBarItem extends StatelessWidget { final VoidCallback onTap; diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index 9d1b858..60c79d4 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -22,6 +22,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:provider/provider.dart'; +import '../audio/player_navbar.dart'; + class DidvanScaffold extends StatefulWidget { final List? slivers; final List? children; @@ -63,11 +65,11 @@ class _DidvanScaffoldState extends State { @override void dispose() { - if(!widget.hidePlayer){ - if(MediaService.currentPodcast != null){ - MediaService.audioPlayer.dispose(); - } - } + // if(!widget.hidePlayer){ + // if(MediaService.currentPodcast != null){ + // MediaService.audioPlayer.dispose(); + // } + // } super.dispose(); } @@ -85,7 +87,7 @@ class _DidvanScaffoldState extends State { child: SizedBox( height: MediaQuery.of(context).size.height - statusBarHeight - - systemNavigationBarHeight, + systemNavigationBarHeight , child: Stack( children: [ CustomScrollView( @@ -143,6 +145,8 @@ class _DidvanScaffoldState extends State { 12, ), ), + if(!widget.hidePlayer) + const SliverPadding(padding: EdgeInsets.only(bottom: 90)) ], ), if (widget.reverse && widget.appBarData != null) @@ -152,10 +156,16 @@ class _DidvanScaffoldState extends State { ), if (!widget.hidePlayer) const Positioned( - bottom: 20, - left: 0, - right: 0, - child: _PlayerNavBar(), + bottom: 16, + left: 16, + right: 16, + child: SizedBox( + child: Center( + child: PlayerNavBar( + inHome: false, + ), + ), + ), ), ], ), @@ -168,6 +178,7 @@ class _DidvanScaffoldState extends State { class _AppBar extends StatefulWidget { final AppBarData appBarData; final ScrollController scrollController; + const _AppBar({ Key? key, required this.appBarData, @@ -209,272 +220,272 @@ class __AppBarState extends State<_AppBar> { } } -class _PlayerNavBar extends StatelessWidget { - const _PlayerNavBar({Key? key}) : super(key: key); - - bool _enablePlayerController(StudioDetailsState state) => - MediaService.currentPodcast != null || - (MediaService.audioPlayerTag?.contains('podcast') ?? false); - - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: MediaService.audioPlayer.isPlaying, - builder: (context, snapshot) => GestureDetector( - onTap: () => MediaService.currentPodcast == null || - MediaService.currentPodcast?.description == 'radar' - ? Navigator.of(context).pushNamed( - Routes.radarDetails, - arguments: { - 'onMarkChanged': (id, value) {}, - 'onCommentsChanged': (id, value) {}, - 'id': MediaService.currentPodcast?.id, - 'args': const RadarRequestArgs(page: 0), - 'hasUnmarkConfirmation': false, - }, - ) - : _showPlayerBottomSheet(context), - child: Consumer( - builder: (context, state, child) => AnimatedContainer( - padding: const EdgeInsets.only(top: 12), - duration: DesignConfig.lowAnimationDuration, - height: _enablePlayerController(state) ? 60 : 0, - margin: const EdgeInsets.symmetric(horizontal: 20), - decoration: BoxDecoration( - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.focused - : Theme.of(context).colorScheme.navigation, - borderRadius: BorderRadius.circular(200), - ), - alignment: Alignment.topCenter, - child: Builder(builder: (context) { - if (!_enablePlayerController(state)) return const SizedBox(); - if (state.appState == AppState.failed) { - Future.delayed(const Duration(seconds: 2), () { - MediaService.resetAudioPlayer(); - }); - return DidvanText( - 'اتصال اینترنت برقرار نمی‌باشد', - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title - : Theme.of(context).colorScheme.secondCTA, - ); - } - if (MediaService.currentPodcast == null) { - return SizedBox( - height: 32, - child: Center( - child: SpinKitThreeBounce( - size: 18, - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title - : Theme.of(context).colorScheme.secondCTA, - ), - ), - ); - } - return SizedBox( - height: 56, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 8, - ), - child: DidvanIconButton( - icon: DidvanIcons.close_regular, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - gestureSize: 32, - onPressed: MediaService.resetAudioPlayer, - ), - ), - SkeletonImage( - imageUrl: MediaService.currentPodcast!.image, - width: 32, - height: 32, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DidvanText( - MediaService.currentPodcast!.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - ), - AudioSlider( - disableThumb: true, - tag: MediaService.audioPlayerTag!, - ), - ], - ), - ), - StreamBuilder( - stream: MediaService.audioPlayer.onReadyToPlay, - builder: (context, snapshot) { - if (snapshot.data == null || - state.appState == AppState.busy && - MediaService.currentPodcast?.description != - 'radar') { - return Padding( - padding: const EdgeInsets.only( - top: 4, - left: 16, - right: 16, - ), - child: SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title - : Theme.of(context).colorScheme.secondCTA, - ), - ), - ); - } - return const SizedBox(); - }, - ), - if (state.appState != AppState.busy && - snapshot.data != null || - MediaService.currentPodcast?.description == 'radar') - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - ), - child: DidvanIconButton( - gestureSize: 32, - color: DesignConfig.isDark - ? null - : Theme.of(context).colorScheme.secondCTA, - icon: snapshot.data! - ? DidvanIcons.pause_solid - : DidvanIcons.play_solid, - onPressed: () { - if (state.args?.type == 'video') { - state.getStudioDetails( - MediaService.currentPodcast!.id, - args: state.podcastArgs, - fetchOnly: true, - ); - } - MediaService.handleAudioPlayback( - audioSource: MediaService.currentPodcast!.link, - id: MediaService.currentPodcast!.id, - isVoiceMessage: false, - ); - }, - ), - ), - ], - ), - ); - }), - ), - ), - ), - ); - } - - void _showPlayerBottomSheet(BuildContext context) { - final sheetKey = GlobalKey(); - bool isExpanded = false; - final detailsState = context.read(); - if (detailsState.args?.type == 'video') { - detailsState.getStudioDetails( - MediaService.currentPodcast!.id, - args: detailsState.podcastArgs, - fetchOnly: true, - ); - } - final state = context.read(); - showModalBottomSheet( - constraints: BoxConstraints( - maxWidth: ActionSheetUtils.mediaQueryData.size.width, - ), - backgroundColor: Colors.transparent, - context: context, - isScrollControlled: true, - builder: (context) => ChangeNotifierProvider.value( - value: state, - child: Consumer( - builder: (context, state, child) => MediaQuery( - data: ActionSheetUtils.mediaQueryData, - child: ExpandableBottomSheet( - key: sheetKey, - background: Align( - alignment: Alignment.bottomCenter, - child: Container( - height: MediaQuery.of(context).size.height * 0.7, - color: Theme.of(context).colorScheme.surface, - ), - ), - persistentHeader: GestureDetector( - onVerticalDragUpdate: (details) { - if (details.delta.dy > 10) { - Navigator.of(context).pop(); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AudioPlayerWidget( - podcast: MediaService.currentPodcast!, - ), - Container( - width: MediaQuery.of(context).size.width, - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - DidvanIconButton( - size: 32, - icon: DidvanIcons.angle_up_regular, - onPressed: () { - if (!isExpanded) { - sheetKey.currentState?.expand(); - isExpanded = true; - } else { - isExpanded = false; - sheetKey.currentState?.contract(); - } - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ], - ), - ), - expandableContent: state.appState == AppState.busy - ? Container( - height: MediaQuery.of(context).size.height / 2, - alignment: Alignment.center, - child: SpinKitSpinningLines( - color: Theme.of(context).colorScheme.primary, - ), - ) - : StudioDetailsWidget( - onMarkChanged: (id, value) => context - .read() - .changeMark(id, value, true), - ), - ), - ), - ), - ), - ); - } -} +// class _PlayerNavBar extends StatelessWidget { +// const _PlayerNavBar({Key? key}) : super(key: key); +// +// bool _enablePlayerController(StudioDetailsState state) => +// MediaService.currentPodcast != null || +// (MediaService.audioPlayerTag?.contains('podcast') ?? false); +// +// +// @override +// Widget build(BuildContext context) { +// return StreamBuilder( +// stream: MediaService.audioPlayer.isPlaying, +// builder: (context, snapshot) => GestureDetector( +// onTap: () => MediaService.currentPodcast == null || +// MediaService.currentPodcast?.description == 'radar' +// ? Navigator.of(context).pushNamed( +// Routes.radarDetails, +// arguments: { +// 'onMarkChanged': (id, value) {}, +// 'onCommentsChanged': (id, value) {}, +// 'id': MediaService.currentPodcast?.id, +// 'args': const RadarRequestArgs(page: 0), +// 'hasUnmarkConfirmation': false, +// }, +// ) +// : _showPlayerBottomSheet(context), +// child: Consumer( +// builder: (context, state, child) => AnimatedContainer( +// padding: const EdgeInsets.only(top: 12), +// duration: DesignConfig.lowAnimationDuration, +// height: _enablePlayerController(state) ? 60 : 0, +// margin: const EdgeInsets.symmetric(horizontal: 20), +// decoration: BoxDecoration( +// color: DesignConfig.isDark +// ? Theme.of(context).colorScheme.focused +// : Theme.of(context).colorScheme.navigation, +// borderRadius: BorderRadius.circular(200), +// ), +// alignment: Alignment.topCenter, +// child: Builder(builder: (context) { +// if (!_enablePlayerController(state)) return const SizedBox(); +// if (state.appState == AppState.failed) { +// Future.delayed(const Duration(seconds: 2), () { +// MediaService.resetAudioPlayer(); +// }); +// return DidvanText( +// 'اتصال اینترنت برقرار نمی‌باشد', +// color: DesignConfig.isDark +// ? Theme.of(context).colorScheme.title +// : Theme.of(context).colorScheme.secondCTA, +// ); +// } +// if (MediaService.currentPodcast == null) { +// return SizedBox( +// height: 32, +// child: Center( +// child: SpinKitThreeBounce( +// size: 18, +// color: DesignConfig.isDark +// ? Theme.of(context).colorScheme.title +// : Theme.of(context).colorScheme.secondCTA, +// ), +// ), +// ); +// } +// return SizedBox( +// height: 56, +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Padding( +// padding: const EdgeInsets.only( +// right: 12, +// left: 8, +// ), +// child: DidvanIconButton( +// icon: DidvanIcons.close_regular, +// color: DesignConfig.isDark +// ? null +// : Theme.of(context).colorScheme.secondCTA, +// gestureSize: 32, +// onPressed: MediaService.resetAudioPlayer, +// ), +// ), +// SkeletonImage( +// imageUrl: MediaService.currentPodcast!.image, +// width: 32, +// height: 32, +// ), +// const SizedBox(width: 16), +// Expanded( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// DidvanText( +// MediaService.currentPodcast!.title, +// maxLines: 1, +// overflow: TextOverflow.ellipsis, +// color: DesignConfig.isDark +// ? null +// : Theme.of(context).colorScheme.secondCTA, +// ), +// AudioSlider( +// disableThumb: true, +// tag: MediaService.audioPlayerTag!, +// ), +// ], +// ), +// ), +// StreamBuilder( +// stream: MediaService.audioPlayer.onReadyToPlay, +// builder: (context, snapshot) { +// if (snapshot.data == null || +// state.appState == AppState.busy && +// MediaService.currentPodcast?.description != +// 'radar') { +// return Padding( +// padding: const EdgeInsets.only( +// top: 4, +// left: 16, +// right: 16, +// ), +// child: SizedBox( +// height: 18, +// width: 18, +// child: CircularProgressIndicator( +// strokeWidth: 2, +// color: DesignConfig.isDark +// ? Theme.of(context).colorScheme.title +// : Theme.of(context).colorScheme.secondCTA, +// ), +// ), +// ); +// } +// return const SizedBox(); +// }, +// ), +// if (state.appState != AppState.busy && +// snapshot.data != null || +// MediaService.currentPodcast?.description == 'radar') +// Padding( +// padding: const EdgeInsets.only( +// left: 12, +// right: 12, +// ), +// child: DidvanIconButton( +// gestureSize: 32, +// color: DesignConfig.isDark +// ? null +// : Theme.of(context).colorScheme.secondCTA, +// icon: snapshot.data! +// ? DidvanIcons.pause_solid +// : DidvanIcons.play_solid, +// onPressed: () { +// if (state.args?.type == 'video') { +// state.getStudioDetails( +// MediaService.currentPodcast!.id, +// args: state.podcastArgs, +// fetchOnly: true, +// ); +// } +// MediaService.handleAudioPlayback( +// audioSource: MediaService.currentPodcast!.link, +// id: MediaService.currentPodcast!.id, +// isVoiceMessage: false, +// ); +// }, +// ), +// ), +// ], +// ), +// ); +// }), +// ), +// ), +// ), +// ); +// } +// +// void _showPlayerBottomSheet(BuildContext context) { +// final sheetKey = GlobalKey(); +// bool isExpanded = false; +// final detailsState = context.read(); +// if (detailsState.args?.type == 'video') { +// detailsState.getStudioDetails( +// MediaService.currentPodcast!.id, +// args: detailsState.podcastArgs, +// fetchOnly: true, +// ); +// } +// final state = context.read(); +// showModalBottomSheet( +// constraints: BoxConstraints( +// maxWidth: ActionSheetUtils.mediaQueryData.size.width, +// ), +// backgroundColor: Colors.transparent, +// context: context, +// isScrollControlled: true, +// builder: (context) => ChangeNotifierProvider.value( +// value: state, +// child: Consumer( +// builder: (context, state, child) => MediaQuery( +// data: ActionSheetUtils.mediaQueryData, +// child: ExpandableBottomSheet( +// key: sheetKey, +// background: Align( +// alignment: Alignment.bottomCenter, +// child: Container( +// height: MediaQuery.of(context).size.height * 0.7, +// color: Theme.of(context).colorScheme.surface, +// ), +// ), +// persistentHeader: GestureDetector( +// onVerticalDragUpdate: (details) { +// if (details.delta.dy > 10) { +// Navigator.of(context).pop(); +// } +// }, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// AudioPlayerWidget( +// podcast: MediaService.currentPodcast!, +// ), +// Container( +// width: MediaQuery.of(context).size.width, +// color: Theme.of(context).colorScheme.surface, +// child: Column( +// children: [ +// DidvanIconButton( +// size: 32, +// icon: DidvanIcons.angle_up_regular, +// onPressed: () { +// if (!isExpanded) { +// sheetKey.currentState?.expand(); +// isExpanded = true; +// } else { +// isExpanded = false; +// sheetKey.currentState?.contract(); +// } +// }, +// ), +// const SizedBox(height: 16), +// ], +// ), +// ), +// ], +// ), +// ), +// expandableContent: state.appState == AppState.busy +// ? Container( +// height: MediaQuery.of(context).size.height / 2, +// alignment: Alignment.center, +// child: SpinKitSpinningLines( +// color: Theme.of(context).colorScheme.primary, +// ), +// ) +// : StudioDetailsWidget( +// onMarkChanged: (id, value) => context +// .read() +// .changeMark(id, value, true), +// ), +// ), +// ), +// ), +// ), +// ); +// } +// } diff --git a/lib/views/widgets/floating_navigation_bar.dart b/lib/views/widgets/floating_navigation_bar.dart index d9b8180..c2daac2 100644 --- a/lib/views/widgets/floating_navigation_bar.dart +++ b/lib/views/widgets/floating_navigation_bar.dart @@ -130,6 +130,7 @@ class _FloatingNavigationBarState extends State { return; } Navigator.of(context).pop(); + }, icon: DidvanIcons.back_regular, ), diff --git a/pubspec.lock b/pubspec.lock index 108c264..60ffdfc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: "752039d6aa752597c98ec212e9759519061759e402e7da59a511f39d43aa07d2" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b + url: "https://pub.dev" + source: hosted + version: "6.0.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: db8fc420dadf80da18e2286c18e746fb4c3b2c5adbf0c963299dde046828886d + url: "https://pub.dev" + source: hosted + version: "5.0.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2" + url: "https://pub.dev" + source: hosted + version: "4.0.0" awesome_notifications: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 164afb0..c109245 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,7 @@ dependencies: android_intent_plus: ^5.0.0 get: ^4.6.6 firebase_auth: ^4.19.6 + audioplayers: ^6.0.0 dev_dependencies: flutter_test: