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/requests/radar.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/widgets/audio/audio_player_widget.dart'; import 'package:didvan/views/widgets/audio/audio_slider.dart'; import 'package:didvan/views/podcasts/podcasts_state.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; import 'package:didvan/views/podcasts/studio_details/widgets/studio_details_widget.dart'; import 'package:didvan/views/widgets/didvan/app_bar.dart'; 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/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:provider/provider.dart'; class DidvanScaffold extends StatefulWidget { final List? slivers; final List? children; final AppBarData? appBarData; final EdgeInsets padding; final Color? backgroundColor; final bool reverse; final ScrollPhysics? physics; final ScrollController? scrollController; final bool showSliversFirst; final bool hidePlayer; const DidvanScaffold({ Key? key, this.slivers, required this.appBarData, this.children, this.physics, this.padding = const EdgeInsets.symmetric(horizontal: 16), this.backgroundColor, this.reverse = false, this.scrollController, this.showSliversFirst = false, this.hidePlayer = false, }) : super(key: key); @override State createState() => _DidvanScaffoldState(); } class _DidvanScaffoldState extends State { late final ScrollController _scrollController; @override void initState() { _scrollController = widget.scrollController ?? ScrollController(); super.initState(); } @override Widget build(BuildContext context) { final double statusBarHeight = MediaQuery.of(context).padding.top; final double systemNavigationBarHeight = MediaQuery.of(context).padding.bottom; return Scaffold( backgroundColor: widget.backgroundColor, body: Padding( padding: widget.appBarData == null ? EdgeInsets.zero : EdgeInsets.only(top: statusBarHeight), child: SizedBox( height: MediaQuery.of(context).size.height - statusBarHeight - systemNavigationBarHeight, child: Stack( children: [ CustomScrollView( physics: widget.physics, controller: _scrollController, reverse: widget.reverse, slivers: [ if (!widget.reverse && widget.appBarData != null) SliverAppBar( toolbarHeight: (widget.appBarData!.isSmall ? 56 : 72) - statusBarHeight, automaticallyImplyLeading: false, pinned: true, backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, elevation: widget.appBarData?.hasElevation == false ? 0 : null, flexibleSpace: DidvanAppBar( appBarData: widget.appBarData!, backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, ), ), if (widget.children != null && !widget.showSliversFirst) SliverPadding( padding: widget.padding.copyWith(bottom: 16), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) => widget.children![index], childCount: widget.children!.length, ), ), ), if (widget.slivers != null) for (var i = 0; i < widget.slivers!.length; i++) SliverPadding( padding: widget.padding, sliver: widget.slivers![i], ), if (widget.children != null && widget.showSliversFirst) SliverPadding( padding: widget.padding, sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) => widget.children![index], childCount: widget.children!.length, ), ), ), if (widget.reverse) SliverToBoxAdapter( child: SizedBox( height: kToolbarHeight + MediaQuery.of(context).padding.top + 12, ), ), ], ), if (widget.reverse && widget.appBarData != null) _AppBar( appBarData: widget.appBarData!, scrollController: _scrollController, ), if (!widget.hidePlayer) const Positioned( bottom: 20, left: 0, right: 0, child: _PlayerNavBar(), ), ], ), ), ), ); } } class _AppBar extends StatefulWidget { final AppBarData appBarData; final ScrollController scrollController; const _AppBar({ Key? key, required this.appBarData, required this.scrollController, }) : super(key: key); @override __AppBarState createState() => __AppBarState(); } class __AppBarState extends State<_AppBar> { bool _isScrolled = false; @override void initState() { widget.scrollController.addListener(() { final position = widget.scrollController.position.pixels; if (position > 10 && _isScrolled == false) { setState(() { _isScrolled = true; }); } if (position < 10 && _isScrolled == true) { setState(() { _isScrolled = false; }); } }); super.initState(); } @override Widget build(BuildContext context) { return DidvanAppBar( backgroundColor: Theme.of(context).colorScheme.surface, appBarData: widget.appBarData, hasBorder: _isScrolled, ); } } 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), ), ), ), ), ), ); } }