didvan-app/lib/views/widgets/didvan/scaffold.dart

481 lines
18 KiB
Dart

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<Widget>? slivers;
final List<Widget>? 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<DidvanScaffold> createState() => _DidvanScaffoldState();
}
class _DidvanScaffoldState extends State<DidvanScaffold> {
late final ScrollController _scrollController;
@override
void initState() {
_scrollController = widget.scrollController ?? ScrollController();
super.initState();
}
@override
void dispose() {
if(!widget.hidePlayer){
if(MediaService.currentPodcast != null){
MediaService.audioPlayer.dispose();
}
}
super.dispose();
}
@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<bool>(
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<StudioDetailsState>(
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<PlayingAudio?>(
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<ExpandableBottomSheetState>();
bool isExpanded = false;
final detailsState = context.read<StudioDetailsState>();
if (detailsState.args?.type == 'video') {
detailsState.getStudioDetails(
MediaService.currentPodcast!.id,
args: detailsState.podcastArgs,
fetchOnly: true,
);
}
final state = context.read<PodcastsState>();
showModalBottomSheet(
constraints: BoxConstraints(
maxWidth: ActionSheetUtils.mediaQueryData.size.width,
),
backgroundColor: Colors.transparent,
context: context,
isScrollControlled: true,
builder: (context) => ChangeNotifierProvider<PodcastsState>.value(
value: state,
child: Consumer<StudioDetailsState>(
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<PodcastsState>()
.changeMark(id, value, true),
),
),
),
),
),
);
}
}