From 873c1f669218753abebcdf6e9961ef7f39701922 Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Wed, 30 Mar 2022 03:37:38 +0430 Subject: [PATCH] bug fixes --- .../studio_details/studio_details.mobile.dart | 5 - .../studio_details/studio_details.web.dart | 5 - .../studio_details/studio_details_state.dart | 13 + lib/views/home/studio/studio_state.dart | 8 + lib/views/home/studio/widgets/slider.dart | 1 + .../widgets/audio/audio_player_widget.dart | 259 ++++++++++------- lib/views/home/widgets/overview/podcast.dart | 38 ++- lib/views/widgets/didvan/bnb.dart | 267 +++++++++++------- 8 files changed, 377 insertions(+), 219 deletions(-) diff --git a/lib/views/home/studio/studio_details/studio_details.mobile.dart b/lib/views/home/studio/studio_details/studio_details.mobile.dart index 378644e..42f0aa7 100644 --- a/lib/views/home/studio/studio_details/studio_details.mobile.dart +++ b/lib/views/home/studio/studio_details/studio_details.mobile.dart @@ -87,11 +87,6 @@ class _StudioDetailsState extends State { state: state, onRetry: () => state.getStudioDetails(state.studio.id), builder: (context, state) { - if (state.prevStudio == null && - state.nextStudio == null && - state.args.page != 0) { - return const SizedBox(); - } return WillPopScope( onWillPop: () async { if (_isFullScreen) { diff --git a/lib/views/home/studio/studio_details/studio_details.web.dart b/lib/views/home/studio/studio_details/studio_details.web.dart index b23740d..f936205 100644 --- a/lib/views/home/studio/studio_details/studio_details.web.dart +++ b/lib/views/home/studio/studio_details/studio_details.web.dart @@ -75,11 +75,6 @@ class _StudioDetailsState extends State { state: state, onRetry: () => state.getStudioDetails(state.studio.id), builder: (context, state) { - if (state.prevStudio == null && - state.nextStudio == null && - state.args.page != 0) { - return const SizedBox(); - } // ignore: undefined_prefixed_name ui.platformViewRegistry.registerViewFactory( "video", diff --git a/lib/views/home/studio/studio_details/studio_details_state.dart b/lib/views/home/studio/studio_details/studio_details_state.dart index fa59702..5151deb 100644 --- a/lib/views/home/studio/studio_details/studio_details_state.dart +++ b/lib/views/home/studio/studio_details/studio_details_state.dart @@ -24,6 +24,7 @@ class StudioDetailsState extends CoreProvier { int _selectedDetailsIndex = 0; Timer? timer; int timerValue = 10; + bool stopOnPodcastEnds = false; int get selectedDetailsIndex => _selectedDetailsIndex; set selectedDetailsIndex(int value) { @@ -45,6 +46,7 @@ class StudioDetailsState extends CoreProvier { if (MediaService.currentPodcast?.id == id && this.args.type == 'podcast') { return; } + _getDownloadsList(); _selectedDetailsIndex = 0; if (isForward != null) { @@ -61,12 +63,19 @@ class StudioDetailsState extends CoreProvier { _handlePodcastPlayback(studio); } if (isForward == null) { + if (this.args.type == 'podcast') { + MediaService.audioPlayerTag = 'podcast'; + } appState = AppState.busy; } final service = RequestService(RequestHelper.studioDetails(id, this.args)); await service.httpGet(); nextStudio = null; prevStudio = null; + if (stopOnPodcastEnds) { + timerValue = 10; + } + stopOnPodcastEnds = false; if (service.isSuccess) { final result = service.result; studio = StudioDetailsData.fromJson(result['studio']); @@ -109,6 +118,10 @@ class StudioDetailsState extends CoreProvier { final duration = MediaService.audioPlayer.duration ?? Duration(seconds: studio.duration); if (event.compareTo(duration) > 0 && nextStudio != null) { + if (stopOnPodcastEnds) { + MediaService.resetAudioPlayer(); + return; + } getStudioDetails(nextStudio!.id, isForward: true); } }); diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart index e168e2d..8d1e40a 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -15,6 +15,9 @@ class StudioState extends CoreProvier { final List studios = []; final List sliders = []; final List downloadedFileIds = []; + final List downloadQueue = []; + + AppState downloadState = AppState.idle; String search = ''; String lastSearch = ''; @@ -147,8 +150,13 @@ class StudioState extends CoreProvier { } Future download(String url, String fileName) async { + downloadState = AppState.busy; + downloadQueue.add(url); + notifyListeners(); final service = RequestService(url); await service.download(fileName, videosSelected ? 'videos' : 'podcasts'); + downloadState = AppState.idle; + downloadQueue.remove(url); notifyListeners(); } } diff --git a/lib/views/home/studio/widgets/slider.dart b/lib/views/home/studio/widgets/slider.dart index c072168..7b6339f 100644 --- a/lib/views/home/studio/widgets/slider.dart +++ b/lib/views/home/studio/widgets/slider.dart @@ -101,6 +101,7 @@ class _StudioSliderState extends State { ), ], options: CarouselOptions( + autoPlayAnimationDuration: DesignConfig.mediumAnimationDuration, onPageChanged: (index, reason) => setState( () => selectedIndex = index, ), diff --git a/lib/views/home/widgets/audio/audio_player_widget.dart b/lib/views/home/widgets/audio/audio_player_widget.dart index 10bbddc..300139d 100644 --- a/lib/views/home/widgets/audio/audio_player_widget.dart +++ b/lib/views/home/widgets/audio/audio_player_widget.dart @@ -4,6 +4,7 @@ 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'; @@ -12,6 +13,7 @@ import 'package:didvan/views/home/studio/studio_details/studio_details_state.dar import 'package:didvan/views/home/studio/studio_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'; @@ -64,95 +66,117 @@ class AudioPlayerWidget extends StatelessWidget { ), Row( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const SizedBox(), - StatefulBuilder( - builder: (context, setState) => Column( - children: [ - DidvanIconButton( - icon: state.timer == null - ? DidvanIcons.sleep_timer_regular - : DidvanIcons.sleep_enabled_regular, - color: Theme.of(context).colorScheme.title, - onPressed: () => _showSleepTimer( - state, - () => setState(() {}), - ), - ), - if (state.timer != null) - DidvanText( - state.timerValue.toString() + '\'', - isEnglishFont: true, - style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.caption, - ), - ], - ), - ), - Column( - children: [ - DidvanIconButton( - color: Theme.of(context).colorScheme.title, - size: 32, - icon: DidvanIcons.media_forward_solid, - onPressed: () { - MediaService.audioPlayer.seek( - Duration( - seconds: - MediaService.audioPlayer.position.inSeconds + 30, - ), - ); - }, - ), - DidvanText( - '30', - isEnglishFont: true, - color: Theme.of(context).colorScheme.title, - ), - ], - ), - StreamBuilder( - stream: MediaService.audioPlayer.playingStream, - builder: (context, snapshot) { - return _PlayPouseAnimatedIcon( - audioSource: podcast.media, - id: podcast.id, - ); - }, - ), - 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.position.inSeconds - 10, + 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.toString(), + isEnglishFont: true, + style: Theme.of(context).textTheme.overline, + 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.position.inSeconds + + 30, + ), + ); + }, + ), + DidvanText( + '30', + isEnglishFont: true, + color: Theme.of(context).colorScheme.title, + ), + ], + ), + ), + ), + Expanded( + child: Center( + child: StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return _PlayPouseAnimatedIcon( + audioSource: podcast.media, + id: podcast.id, ); }, ), - DidvanText( - '10', - isEnglishFont: true, - color: Theme.of(context).colorScheme.title, + ), + ), + 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.position.inSeconds - + 10, + ), + ), + ); + }, + ), + DidvanText( + '10', + isEnglishFont: true, + color: Theme.of(context).colorScheme.title, + ), + ], ), - ], + ), ), - BookmarkButton( - gestureSize: 48, - color: Theme.of(context).colorScheme.title, - value: podcast.marked, - onMarkChanged: (value) => - context.read().changeMark(podcast.id, value), + Expanded( + child: Center( + child: BookmarkButton( + gestureSize: 48, + color: Theme.of(context).colorScheme.title, + value: podcast.marked, + onMarkChanged: (value) => context + .read() + .changeMark(podcast.id, value), + ), + ), ), - const SizedBox(), ], ), ], @@ -163,13 +187,17 @@ class AudioPlayerWidget extends StatelessWidget { Future _showSleepTimer(StudioDetailsState state, update) async { int timerValue = 10; final controller = FixedExtentScrollController(); + bool isInit = true; Future.delayed( const Duration(milliseconds: 100), - () => controller.animateTo( - 50 * (state.timerValue / 5 - 2), - duration: DesignConfig.lowAnimationDuration, - curve: Curves.easeIn, - ), + () async { + await controller.animateTo( + state.timerValue * 10, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + isInit = false; + }, ); await ActionSheetUtils.showBottomSheet( data: ActionSheetData( @@ -193,38 +221,71 @@ class AudioPlayerWidget extends StatelessWidget { child: RotatedBox( quarterTurns: 3, child: ListWheelScrollView( - controller: controller, physics: const FixedExtentScrollPhysics(), - itemExtent: 48, + controller: controller, + itemExtent: 10, onSelectedItemChanged: (index) { - final minutes = (index + 2) * 5; + if (!isInit) { + state.stopOnPodcastEnds = false; + } + final minutes = index == 0 ? 1 : index; timerValue = minutes; setState(() {}); }, children: [ - for (var i = 0; i < 9; i++) - Center( - child: Container( - color: Theme.of(context).colorScheme.text, - width: 50, - height: 3, + 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.audioPlayer.duration?.inMinutes && + state.stopOnPodcastEnds + ? ButtonStyleMode.primary + : ButtonStyleMode.flat, + title: 'پایان پادکست', + onPressed: () async { + state.timerValue = + MediaService.audioPlayer.duration!.inMinutes - + MediaService.audioPlayer.position.inMinutes; + await controller.animateTo( + state.timerValue * 10, + duration: DesignConfig.lowAnimationDuration, + curve: Curves.easeIn, + ); + state.stopOnPodcastEnds = true; + setState(() {}); + }, + ), + ), ], ), ), onConfirmed: () { - state.timer = Timer( - Duration(minutes: timerValue), - MediaService.audioPlayer.stop, - ); + if (!state.stopOnPodcastEnds) { + state.timer = Timer( + Duration(minutes: timerValue), + MediaService.audioPlayer.stop, + ); + } state.timerValue = timerValue; update(); }, + confrimTitle: 'شروع زمان خواب', dismissTitle: 'لغو', onDismissed: () { state.timer?.cancel(); diff --git a/lib/views/home/widgets/overview/podcast.dart b/lib/views/home/widgets/overview/podcast.dart index b42eccf..f4cef1b 100644 --- a/lib/views/home/widgets/overview/podcast.dart +++ b/lib/views/home/widgets/overview/podcast.dart @@ -1,5 +1,6 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/utils/date_time.dart'; @@ -81,19 +82,30 @@ class PodcastOverview extends StatelessWidget { DurationWidget(duration: podcast.duration!), const Spacer(), if (!kIsWeb) ...[ - DidvanIconButton( - gestureSize: 28, - color: _isDownloaded(state) - ? Theme.of(context).colorScheme.primary - : null, - icon: _isDownloaded(state) - ? DidvanIcons.download_solid - : DidvanIcons.download_regular, - onPressed: _isDownloaded(state) - ? () {} - : () => - state.download(podcast.media!, podcast.id.toString()), - ), + if (state.downloadState == AppState.idle || + !state.downloadQueue.contains(podcast.media)) + DidvanIconButton( + gestureSize: 28, + color: _isDownloaded(state) + ? Theme.of(context).colorScheme.primary + : null, + icon: _isDownloaded(state) + ? DidvanIcons.download_solid + : DidvanIcons.download_regular, + onPressed: _isDownloaded(state) + ? () {} + : () => state.download( + podcast.media!, podcast.id.toString()), + ), + if (state.downloadState == AppState.busy && + state.downloadQueue.contains(podcast.media)) + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), const SizedBox(width: 16), ], BookmarkButton( diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart index 9982251..3c4b804 100644 --- a/lib/views/widgets/didvan/bnb.dart +++ b/lib/views/widgets/didvan/bnb.dart @@ -14,6 +14,8 @@ 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/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:provider/provider.dart'; class DidvanBNB extends StatelessWidget { @@ -24,109 +26,169 @@ class DidvanBNB extends StatelessWidget { {Key? key, required this.currentTabIndex, required this.onTabChanged}) : super(key: key); - bool get _enablePlayerController => - MediaService.currentPodcast != null && + bool _enablePlayerController(StudioDetailsState state) => + MediaService.currentPodcast != null || (MediaService.audioPlayerTag?.contains('podcast') ?? false); @override Widget build(BuildContext context) { - final state = context.read(); return StreamBuilder( stream: MediaService.audioPlayer.playingStream, builder: (context, snapshot) { return Stack( children: [ GestureDetector( - onTap: () => _showPlayerBottomSheet(context), - child: AnimatedContainer( - padding: const EdgeInsets.only(top: 12), - duration: DesignConfig.lowAnimationDuration, - height: _enablePlayerController ? 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), + onTap: () => MediaService.currentPodcast == null + ? null + : _showPlayerBottomSheet(context), + 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), + ), ), - ), - child: !_enablePlayerController - ? const SizedBox() - : 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: MediaService.resetAudioPlayer, - ), - ), - SkeletonImage( - imageUrl: MediaService.currentPodcast!.image, - width: 32, + alignment: Alignment.topCenter, + child: !_enablePlayerController(state) + ? const SizedBox() + : MediaService.currentPodcast == null + ? SizedBox( height: 32, - ), - const SizedBox(width: 16), - Expanded( - child: Column( + child: Center( + child: SpinKitThreeBounce( + size: 18, + color: DesignConfig.isDark + ? null + : Theme.of(context) + .colorScheme + .secondCTA, + ), + ), + ) + : SizedBox( + height: 56, + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - DidvanText( - MediaService.currentPodcast!.title, - color: DesignConfig.isDark - ? null - : Theme.of(context) - .colorScheme - .secondCTA, + 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, + ), ), - AudioSlider( - disableThumb: true, - tag: MediaService.audioPlayerTag!, + SkeletonImage( + imageUrl: + MediaService.currentPodcast!.image, + width: 32, + height: 32, ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + DidvanText( + MediaService.currentPodcast!.title, + color: DesignConfig.isDark + ? null + : Theme.of(context) + .colorScheme + .secondCTA, + ), + AudioSlider( + disableThumb: true, + tag: MediaService.audioPlayerTag!, + ), + ], + ), + ), + StreamBuilder( + stream: MediaService + .audioPlayer.playerStateStream, + builder: (context, snapshot) { + if (state.appState == AppState.busy || + MediaService.audioPlayer.playerState + .processingState == + ProcessingState.loading) { + return Padding( + padding: const EdgeInsets.only( + top: 4, + left: 16, + right: 16, + ), + child: SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context) + .colorScheme + .secondCTA, + ), + ), + ); + } + return const SizedBox(); + }, + ), + if (state.appState != AppState.busy && + MediaService.audioPlayer.playerState + .processingState != + ProcessingState.loading) + 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: () { + MediaService.handleAudioPlayback( + audioSource: state + .downloadedFileIds + .contains(state.studio.id) + ? StorageService.appDocsDir + + '/podcasts/podcast-${state.studio.id}.mp3' + : state.studio.media, + isNetworkAudio: !state + .downloadedFileIds + .contains(state.studio.id), + id: state.studio.id, + isVoiceMessage: false, + ); + }, + ), + ), ], ), ), - 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: () { - MediaService.handleAudioPlayback( - audioSource: state.downloadedFileIds - .contains(state.studio.id) - ? StorageService.appDocsDir + - '/podcasts/podcast-${state.studio.id}.mp3' - : state.studio.media, - isNetworkAudio: !state.downloadedFileIds - .contains(state.studio.id), - id: state.studio.id, - isVoiceMessage: false, - ); - }, - ), - ), - ], - ), - ), + ), ), ), Positioned( @@ -226,18 +288,23 @@ class DidvanBNB extends StatelessWidget { color: Theme.of(context).colorScheme.surface, child: Column( children: [ - DidvanIconButton( - size: 32, - icon: DidvanIcons.angle_down_regular, - onPressed: () { - if (!isExpanded) { - sheetKey.currentState?.expand(); - isExpanded = true; - return; - } - isExpanded = false; - sheetKey.currentState?.contract(); - }, + StatefulBuilder( + builder: (context, setState) => DidvanIconButton( + size: 32, + icon: isExpanded + ? DidvanIcons.angle_down_regular + : DidvanIcons.angle_up_regular, + onPressed: () { + if (!isExpanded) { + sheetKey.currentState?.expand(); + isExpanded = true; + } else { + isExpanded = false; + sheetKey.currentState?.contract(); + } + setState(() {}); + }, + ), ), const SizedBox(height: 16), ], @@ -247,7 +314,13 @@ class DidvanBNB extends StatelessWidget { ), ), expandableContent: state.appState == AppState.busy - ? const SizedBox() + ? Container( + height: MediaQuery.of(context).size.height / 2, + alignment: Alignment.center, + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ) : StudioDetailsWidget( studio: detailsState.studio, onCommentsTabSelected: () {