diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 7e173cf..fb0fe74 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -7,6 +7,8 @@ import 'package:just_audio/just_audio.dart'; class MediaService { static final AudioPlayer audioPlayer = AudioPlayer(); static String? audioPlayerTag; + static String? audioPlayerTitle; + static String? audioPlayerCover; static void init() { audioPlayer.positionStream.listen((event) { @@ -54,6 +56,11 @@ class MediaService { } } + static Future resetAudioPlayer() async { + audioPlayerTag = null; + MediaService.audioPlayer.stop(); + } + static Future pickImage({required ImageSource source}) async { final imagePicker = ImagePicker(); final XFile? pickedFile = await imagePicker.pickImage(source: source); diff --git a/lib/views/home/direct/direct.dart b/lib/views/home/direct/direct.dart index e3cf43e..2af96ec 100644 --- a/lib/views/home/direct/direct.dart +++ b/lib/views/home/direct/direct.dart @@ -1,6 +1,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/providers/server_data_provider.dart'; +import 'package:didvan/services/media/media.dart'; import 'package:didvan/views/home/direct/direct_state.dart'; import 'package:didvan/views/home/direct/widgets/message.dart'; import 'package:didvan/views/home/direct/widgets/message_box.dart'; @@ -35,59 +36,65 @@ class _DirectState extends State { Widget build(BuildContext context) { final state = context.watch(); final d = MediaQuery.of(context); - return Material( - child: Stack( - children: [ - Positioned( - top: 0, - bottom: 56, - left: 0, - right: 0, - child: DidvanScaffold( - reverse: true, - backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: AppBarData( - hasBack: true, - subtitle: 'ارتباط با سردبیر', - title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', + return WillPopScope( + onWillPop: () async { + MediaService.resetAudioPlayer(); + return true; + }, + child: Material( + child: Stack( + children: [ + Positioned( + top: 0, + bottom: 56, + left: 0, + right: 0, + child: DidvanScaffold( + reverse: true, + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: AppBarData( + hasBack: true, + subtitle: 'ارتباط با سردبیر', + title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', + ), + slivers: [ + if (state.appState != AppState.busy) + SliverPadding( + padding: state.replyRadar == null + ? EdgeInsets.zero + : const EdgeInsets.only(bottom: 68), + sliver: SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 12), + state: state, + builder: (context, state, index) => Message( + message: state.messages[index], + ), + childCount: state.messages.length, + onRetry: state.getMessages, + ), + ), + ], + children: [ + if (state.appState == AppState.busy) + SizedBox( + height: d.size.height - kToolbarHeight - d.padding.top, + child: Center( + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], ), - slivers: [ - if (state.appState != AppState.busy) - SliverPadding( - padding: state.replyRadar == null - ? EdgeInsets.zero - : const EdgeInsets.only(bottom: 68), - sliver: SliverStateHandler( - itemPadding: const EdgeInsets.only(bottom: 12), - state: state, - builder: (context, state, index) => Message( - message: state.messages[index], - ), - childCount: state.messages.length, - onRetry: state.getMessages, - ), - ), - ], - children: [ - if (state.appState == AppState.busy) - SizedBox( - height: d.size.height - kToolbarHeight - d.padding.top, - child: Center( - child: SpinKitSpinningLines( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], ), - ), - Positioned( - bottom: d.viewInsets.bottom, - right: 0, - left: 0, - child: const MessageBox(), - ), - ], + Positioned( + bottom: d.viewInsets.bottom, + right: 0, + left: 0, + child: const MessageBox(), + ), + ], + ), ), ); } diff --git a/lib/views/home/studio/podcast_details/podcast_details.dart b/lib/views/home/studio/podcast_details/podcast_details.dart new file mode 100644 index 0000000..140d8d0 --- /dev/null +++ b/lib/views/home/studio/podcast_details/podcast_details.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PodcastDetails extends StatelessWidget { + const PodcastDetails({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + ), + ), + ); + } +} diff --git a/lib/views/home/studio/video_details/video_details.dart b/lib/views/home/studio/video_details/video_details.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/views/home/studio/widgets/tab_bar.dart b/lib/views/home/studio/widgets/tab_bar.dart index c5caacb..7e11744 100644 --- a/lib/views/home/studio/widgets/tab_bar.dart +++ b/lib/views/home/studio/widgets/tab_bar.dart @@ -1,8 +1,10 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class StudioTabBar extends StatelessWidget { const StudioTabBar({ @@ -11,11 +13,16 @@ class StudioTabBar extends StatelessWidget { @override Widget build(BuildContext context) { + final state = context.watch(); return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(4), decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).colorScheme.border), + border: Border.all( + color: state.videosSelected + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).primaryColor, + ), borderRadius: DesignConfig.lowBorderRadius, ), child: Row( @@ -26,7 +33,7 @@ class StudioTabBar extends StatelessWidget { selectedColor: Theme.of(context).colorScheme.secondary, title: 'ویدئو', onTap: () {}, - isSelected: true, + isSelected: state.videosSelected, ), ), Container( @@ -40,7 +47,7 @@ class StudioTabBar extends StatelessWidget { selectedColor: Theme.of(context).colorScheme.focusedBorder, title: 'پادکست', onTap: () {}, - isSelected: true, + isSelected: !state.videosSelected, ), ), ], @@ -64,7 +71,8 @@ class _StudioTypeButton extends StatelessWidget { required this.isSelected, }) : super(key: key); - Color? get _color => isSelected ? selectedColor : null; + Color? _color(context) => + isSelected ? selectedColor : Theme.of(context).colorScheme.hint; @override Widget build(BuildContext context) { @@ -77,20 +85,20 @@ class _StudioTypeButton extends StatelessWidget { Icon( icon, size: 32, - color: _color, + color: _color(context), ), if (!isSelected) const SizedBox(height: 18), if (isSelected) Container( width: 88, height: 1, - color: _color, + color: _color(context), ), if (isSelected) DidvanText( title, style: Theme.of(context).textTheme.overline, - color: _color, + color: _color(context), ) ], ), diff --git a/lib/views/home/widgets/bnb.dart b/lib/views/home/widgets/bnb.dart index 42d8ad9..4c78de0 100644 --- a/lib/views/home/widgets/bnb.dart +++ b/lib/views/home/widgets/bnb.dart @@ -1,7 +1,10 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/services/media/media.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:flutter/material.dart'; class DidvanBNB extends StatelessWidget { @@ -14,54 +17,93 @@ class DidvanBNB extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: 72, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - boxShadow: DesignConfig.defaultShadow, - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - _NavBarItem( - isSelected: currentTabIndex == 0, - title: 'اخبار', - selectedIcon: DidvanIcons.news_solid, - unselectedIcon: DidvanIcons.news_light, - onTap: () => onTabChanged(0), - ), - _NavBarItem( - isSelected: currentTabIndex == 1, - title: 'آمار', - selectedIcon: DidvanIcons.chart_solid, - unselectedIcon: DidvanIcons.chart_light, - onTap: () => onTabChanged(1), - ), - _NavBarItem( - isSelected: currentTabIndex == 2, - title: 'رادار', - selectedIcon: DidvanIcons.radar_solid, - unselectedIcon: DidvanIcons.radar_light, - onTap: () => onTabChanged(2), - ), - _NavBarItem( - isSelected: currentTabIndex == 3, - title: 'استودیو', - selectedIcon: DidvanIcons.play_circle_solid, - unselectedIcon: DidvanIcons.play_circle_light, - onTap: () => onTabChanged(3), - ), - _NavBarItem( - isSelected: currentTabIndex == 4, - title: 'تنظیمات', - selectedIcon: DidvanIcons.setting_solid, - unselectedIcon: DidvanIcons.setting_light, - onTap: () => onTabChanged(4), - ), - ], - ), - ); + return StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + return Stack( + children: [ + AnimatedContainer( + duration: DesignConfig.lowAnimationDuration, + height: snapshot.data == true ? 120 : 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: Row( + children: [ + const DidvanIconButton( + icon: DidvanIcons.close_regular, + gestureSize: 24, + onPressed: MediaService.resetAudioPlayer, + ), + const SizedBox(width: 16), + if (MediaService.audioPlayerCover != null) + SkeletonImage(imageUrl: MediaService.audioPlayerCover!), + const SizedBox(width: 16), + ], + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: DesignConfig.defaultShadow, + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _NavBarItem( + isSelected: currentTabIndex == 0, + title: 'اخبار', + selectedIcon: DidvanIcons.news_solid, + unselectedIcon: DidvanIcons.news_light, + onTap: () => onTabChanged(0), + ), + _NavBarItem( + isSelected: currentTabIndex == 1, + title: 'آمار', + selectedIcon: DidvanIcons.chart_solid, + unselectedIcon: DidvanIcons.chart_light, + onTap: () => onTabChanged(1), + ), + _NavBarItem( + isSelected: currentTabIndex == 2, + title: 'رادار', + selectedIcon: DidvanIcons.radar_solid, + unselectedIcon: DidvanIcons.radar_light, + onTap: () => onTabChanged(2), + ), + _NavBarItem( + isSelected: currentTabIndex == 3, + title: 'استودیو', + selectedIcon: DidvanIcons.play_circle_solid, + unselectedIcon: DidvanIcons.play_circle_light, + onTap: () => onTabChanged(3), + ), + _NavBarItem( + isSelected: currentTabIndex == 4, + title: 'تنظیمات', + selectedIcon: DidvanIcons.setting_solid, + unselectedIcon: DidvanIcons.setting_light, + onTap: () => onTabChanged(4), + ), + ], + ), + ), + ), + ], + ); + }); } } diff --git a/lib/views/home/widgets/podcast_overview.dart b/lib/views/home/widgets/podcast_overview.dart new file mode 100644 index 0000000..fa626fb --- /dev/null +++ b/lib/views/home/widgets/podcast_overview.dart @@ -0,0 +1,138 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/requests/news.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/card.dart'; +import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class PodcastOverview extends StatelessWidget { + final OverviewData news; + final NewsRequestArgs? newsRequestArgs; + final void Function(int id, bool value) onMarkChanged; + final bool hasUnmarkConfirmation; + const PodcastOverview({ + Key? key, + required this.news, + required this.onMarkChanged, + this.newsRequestArgs, + this.hasUnmarkConfirmation = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DidvanCard( + onTap: () => Navigator.of(context).pushNamed( + Routes.newsDetails, + arguments: { + 'onMarkChanged': onMarkChanged, + 'id': news.id, + 'args': newsRequestArgs, + 'hasUnmarkConfirmation': hasUnmarkConfirmation, + }, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SkeletonImage( + imageUrl: news.image, + width: 64, + height: 64, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 64, + child: DidvanText( + news.title, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + DidvanText( + news.description, + maxLines: 3, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + DidvanText( + news.reference!, + style: Theme.of(context).textTheme.caption, + ), + DidvanText( + ' - ' + DateTimeUtils.momentGenerator(news.createdAt), + style: Theme.of(context).textTheme.caption, + ), + ], + ), + BookmarkButton( + value: news.marked, + onMarkChanged: (value) => onMarkChanged(news.id, value), + askForConfirmation: hasUnmarkConfirmation, + ), + ], + ), + ], + ), + ); + } + + static Widget get placeholder => DidvanCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ShimmerPlaceholder(height: 64, width: 64), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerPlaceholder(height: 18, width: 200), + SizedBox(height: 8), + ShimmerPlaceholder(height: 18, width: 100), + ], + ), + ], + ), + const SizedBox(height: 12), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: double.infinity, + ), + const SizedBox(height: 8), + const ShimmerPlaceholder( + height: 16, + width: 100, + ), + const DidvanDivider(verticalPadding: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + ShimmerPlaceholder(height: 12, width: 150), + ShimmerPlaceholder(height: 24, width: 24), + ], + ), + ], + ), + ); +}