From 386ed9010187de901af909d27217ecc8cc676f32 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Sun, 19 Oct 2025 07:50:50 +0330 Subject: [PATCH] redesign podcast tab --- lib/assets/icons/backward-5-seconds.svg | 4 + lib/assets/icons/dislike.svg | 4 + lib/assets/icons/dislike_fill.svg | 4 + lib/assets/icons/forward-10-seconds.svg | 5 + lib/assets/icons/like.svg | 4 + lib/assets/icons/likefill.svg | 4 + lib/assets/icons/pause-circle.svg | 3 + lib/assets/icons/play.svg | 3 + lib/assets/icons/send.svg | 4 + lib/assets/icons/send2.svg | 4 + lib/config/theme_data.dart | 2 +- lib/services/media/media.dart | 2 + lib/views/comments/comments.dart | 265 ++++---- lib/views/comments/widgets/comment.dart | 439 ++++++++----- lib/views/home/media/podcast_tab_page.dart | 212 ++++++- lib/views/home/media/video_details_page.dart | 596 +++++++++++------- .../widgets/audio_waveform_progress.dart | 132 ++++ .../media/widgets/featured_podcast_card.dart | 376 +++++++++++ .../home/media/widgets/podcast_list_card.dart | 138 ++++ .../studio_details/studio_details.mobile.dart | 2 +- lib/views/widgets/audio/player_navbar.dart | 376 +++++------ lib/views/widgets/overview/multitype.dart | 54 +- 22 files changed, 1914 insertions(+), 719 deletions(-) create mode 100644 lib/assets/icons/backward-5-seconds.svg create mode 100644 lib/assets/icons/dislike.svg create mode 100644 lib/assets/icons/dislike_fill.svg create mode 100644 lib/assets/icons/forward-10-seconds.svg create mode 100644 lib/assets/icons/like.svg create mode 100644 lib/assets/icons/likefill.svg create mode 100644 lib/assets/icons/pause-circle.svg create mode 100644 lib/assets/icons/play.svg create mode 100644 lib/assets/icons/send.svg create mode 100644 lib/assets/icons/send2.svg create mode 100644 lib/views/home/media/widgets/audio_waveform_progress.dart create mode 100644 lib/views/home/media/widgets/featured_podcast_card.dart create mode 100644 lib/views/home/media/widgets/podcast_list_card.dart diff --git a/lib/assets/icons/backward-5-seconds.svg b/lib/assets/icons/backward-5-seconds.svg new file mode 100644 index 0000000..4021b5e --- /dev/null +++ b/lib/assets/icons/backward-5-seconds.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/dislike.svg b/lib/assets/icons/dislike.svg new file mode 100644 index 0000000..9296e56 --- /dev/null +++ b/lib/assets/icons/dislike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/dislike_fill.svg b/lib/assets/icons/dislike_fill.svg new file mode 100644 index 0000000..a3ae69d --- /dev/null +++ b/lib/assets/icons/dislike_fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/forward-10-seconds.svg b/lib/assets/icons/forward-10-seconds.svg new file mode 100644 index 0000000..e5f9e67 --- /dev/null +++ b/lib/assets/icons/forward-10-seconds.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/like.svg b/lib/assets/icons/like.svg new file mode 100644 index 0000000..8fa0663 --- /dev/null +++ b/lib/assets/icons/like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/likefill.svg b/lib/assets/icons/likefill.svg new file mode 100644 index 0000000..69ef79d --- /dev/null +++ b/lib/assets/icons/likefill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/pause-circle.svg b/lib/assets/icons/pause-circle.svg new file mode 100644 index 0000000..745fcc7 --- /dev/null +++ b/lib/assets/icons/pause-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/play.svg b/lib/assets/icons/play.svg new file mode 100644 index 0000000..a23c8e3 --- /dev/null +++ b/lib/assets/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/send.svg b/lib/assets/icons/send.svg new file mode 100644 index 0000000..6c076f1 --- /dev/null +++ b/lib/assets/icons/send.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/send2.svg b/lib/assets/icons/send2.svg new file mode 100644 index 0000000..3ac5208 --- /dev/null +++ b/lib/assets/icons/send2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/config/theme_data.dart b/lib/config/theme_data.dart index c8308c8..e65c908 100644 --- a/lib/config/theme_data.dart +++ b/lib/config/theme_data.dart @@ -196,7 +196,7 @@ extension DidvanColorScheme on ColorScheme { : const Color(0xFFE0E0E0); Color get cardBorder => brightness == Brightness.dark ? const Color(0xFF5C5C5C) - : const Color(0xFFEBEBEB); + : const Color.fromARGB(255, 184, 184, 184); Color get disabledBackground => brightness == Brightness.dark ? const Color(0xFF333333) : const Color(0xFFE0E0E0); diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 6e61877..9d9ba74 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -27,6 +27,7 @@ class MediaService { static StudioDetailsData? currentPodcast; static StudioRequestArgs? podcastPlaylistArgs; static int maxSizeInBytes = 15 * 1024 * 1024; + static bool isPlayingFromFeaturedCard = false; static Duration? get duration => audioPlayer.duration; @@ -99,6 +100,7 @@ class MediaService { audioPlayerTag = null; currentPodcast = null; podcastPlaylistArgs = null; + isPlayingFromFeaturedCard = false; await MediaService.audioPlayer.stop(); } diff --git a/lib/views/comments/comments.dart b/lib/views/comments/comments.dart index 1488eff..59596bf 100644 --- a/lib/views/comments/comments.dart +++ b/lib/views/comments/comments.dart @@ -1,18 +1,16 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/comments/comments_state.dart'; import 'package:didvan/views/comments/widgets/comment.dart'; -import 'package:didvan/views/widgets/animated_visibility.dart'; -import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class Comments extends StatefulWidget { @@ -32,15 +30,17 @@ class _CommentsState extends State { @override void initState() { - final state = context.read(); - state.itemId = widget.pageData['id']; - state.type = widget.pageData['type']; - state.onCommentsChanged = widget.pageData['onCommentsChanged']; - Future.delayed( - Duration.zero, - () => state.getComments(), - ); super.initState(); + if (_isPage) { + final state = context.read(); + state.itemId = widget.pageData['id']; + state.type = widget.pageData['type']; + state.onCommentsChanged = widget.pageData['onCommentsChanged']; + Future.delayed( + Duration.zero, + () => state.getComments(), + ); + } } bool get _isPage => widget.pageData['isPage'] != false; @@ -56,52 +56,42 @@ class _CommentsState extends State { } _bottomPadding = bottomViewInset; return Material( - child: Stack( - children: [ - DidvanScaffold( - hidePlayer: true, - physics: const BouncingScrollPhysics(), - backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: _isPage - ? AppBarData( - hasBack: true, - title: 'نظرات', - subtitle: widget.pageData['title'], - ) - : null, - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 92), - slivers: [ - Consumer( - builder: (context, state, child) => - SliverStateHandler( - onRetry: state.getComments, - state: state, - itemPadding: const EdgeInsets.symmetric(vertical: 16), - childCount: state.comments.length, - placeholder: const _CommentPlaceholder(), - centerEmptyState: _isPage, - enableEmptyState: state.comments.isEmpty, - emptyState: EmptyState( - asset: Assets.emptyChat, - title: 'اولین نظر را بنویسید...', - ), - builder: (context, state, index) => Comment( - key: ValueKey( - state.comments[index].id.toString() + - state.comments[index].text, - ), - focusNode: _focusNode, - comment: state.comments[index], - ), - ), + child: DidvanScaffold( + hidePlayer: true, + physics: const BouncingScrollPhysics(), + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: _isPage + ? AppBarData( + hasBack: true, + title: 'نظرات', + subtitle: widget.pageData['title'], + ) + : null, + padding: _isPage ? const EdgeInsets.only(bottom: 92) : EdgeInsets.zero, + slivers: [ + Consumer( + builder: (context, state, child) => + SliverStateHandler( + onRetry: state.getComments, + state: state, + itemPadding: const EdgeInsets.only(top: 16, bottom: 16), + childCount: state.comments.length, + placeholder: const _CommentPlaceholder(), + centerEmptyState: _isPage, + enableEmptyState: state.comments.isEmpty, + emptyState: EmptyState( + asset: Assets.emptyChat, + title: 'اولین نظر را بنویسید...', ), - ], - ), - Positioned( - left: 0, - right: 0, - bottom: MediaQuery.of(context).viewInsets.bottom, - child: _MessageBox(focusNode: _focusNode), + builder: (context, state, index) => Comment( + key: ValueKey( + state.comments[index].id.toString() + + state.comments[index].text, + ), + focusNode: _focusNode, + comment: state.comments[index], + ), + ), ), ], ), @@ -109,77 +99,86 @@ class _CommentsState extends State { } } -class _MessageBox extends StatefulWidget { +class CommentMessageBox extends StatefulWidget { final FocusNode focusNode; - const _MessageBox({Key? key, required this.focusNode}) : super(key: key); + const CommentMessageBox({Key? key, required this.focusNode}) + : super(key: key); @override - State<_MessageBox> createState() => _MessageBoxState(); + State createState() => _CommentMessageBoxState(); } -class _MessageBoxState extends State<_MessageBox> { +class _CommentMessageBoxState extends State { final _controller = TextEditingController(); @override Widget build(BuildContext context) { final state = context.watch(); + return Column( children: [ - AnimatedVisibility( - duration: DesignConfig.lowAnimationDuration, - isVisible: state.showReplyBox, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.border, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (state.replyingTo != null) - DidvanText( - 'پاسخ به ${state.replyingTo!.fullName}:', - color: Theme.of(context).colorScheme.caption, - style: Theme.of(context).textTheme.bodySmall, + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: state.showReplyBox + ? Container( + key: const ValueKey('reply_box'), + margin: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 1, + ), ), - const Spacer(), - DidvanIconButton( - gestureSize: 24, - color: Theme.of(context).colorScheme.caption, - icon: DidvanIcons.close_regular, - onPressed: () { - state.commentId = null; - state.replyingTo = null; - state.showReplyBox = false; - state.update(); - }, - ), - ], - ), - ), + child: Row( + children: [ + const Icon( + Icons.reply, + size: 16, + color: Color.fromARGB(255, 194, 194, 194), + ), + const SizedBox(width: 8), + Expanded( + child: DidvanText( + 'پاسخ به ${state.replyingTo?.fullName ?? ""}', + style: const TextStyle( + fontSize: 13, + color: Color.fromARGB(255, 194, 194, 194), + fontWeight: FontWeight.w600, + ), + ), + ), + GestureDetector( + onTap: () { + state.commentId = null; + state.replyingTo = null; + state.showReplyBox = false; + state.update(); + }, + child: const Icon( + Icons.close, + size: 16, + color: Colors.grey, + ), + ), + ], + ), + ) + : const SizedBox.shrink(key: ValueKey('empty')), ), Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.border, - ), - ), + color: const Color.fromARGB(255, 243, 243, 243), + border: Border.all(color: Theme.of(context).colorScheme.border), + borderRadius: BorderRadius.circular(32), ), child: Row( children: [ - DidvanIconButton( - onPressed: () => _onSend(state), - icon: DidvanIcons.send_solid, - size: 24, - color: Theme.of(context).colorScheme.focusedBorder, - ), Expanded( child: TextField( focusNode: widget.focusNode, @@ -191,13 +190,52 @@ class _MessageBoxState extends State<_MessageBox> { onSubmitted: (value) => _onSend(state), decoration: InputDecoration( border: InputBorder.none, - hintText: 'پیام خود را ارسال کنید', - hintStyle: Theme.of(context).textTheme.bodySmall!.copyWith( - color: Theme.of(context).colorScheme.disabledText), + hintText: 'دیدگاه‌های خود را بنویسید...', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black), ), onChanged: (value) => state.text = value, ), ), + ElevatedButton( + onPressed: () => _onSend(state), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(32), + ), + padding: EdgeInsets.zero, + minimumSize: const Size(10, 20), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: Theme.of(context).primaryColor, + elevation: 0, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'ارسال', + style: + Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.normal, + ), + ), + const SizedBox(width: 8), + SvgPicture.asset( + 'lib/assets/icons/send2.svg', + color: Colors.white, + ), + ], + ), + ), + ), + ), ], ), ), @@ -206,9 +244,10 @@ class _MessageBoxState extends State<_MessageBox> { } void _onSend(CommentsState state) { - if (state.text.replaceAll(' ', '').isNotEmpty) { + if (state.text.trim().isNotEmpty) { state.addComment(); - _controller.text = ''; + _controller.clear(); + FocusScope.of(context).unfocus(); } } } diff --git a/lib/views/comments/widgets/comment.dart b/lib/views/comments/widgets/comment.dart index d8a18ef..1dc0d20 100644 --- a/lib/views/comments/widgets/comment.dart +++ b/lib/views/comments/widgets/comment.dart @@ -15,6 +15,7 @@ import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/ink_wrapper.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class Comment extends StatefulWidget { @@ -70,142 +71,168 @@ class CommentState extends State { : BorderSide.none, ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isReply) const SizedBox(width: 12), - if (comment.user.photo == null) - const Icon(DidvanIcons.avatar_light), - if (comment.user.photo != null) - SkeletonImage( - imageUrl: comment.user.photo, - height: 24, - width: 24, - borderRadius: DesignConfig.highBorderRadius, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isReply) const SizedBox(width: 12), + if (comment.user.photo == null) + const Icon( + DidvanIcons.avatar_light, + size: 50, + ), + if (comment.user.photo != null) + SkeletonImage( + imageUrl: comment.user.photo, + height: 50, + width: 50, + borderRadius: DesignConfig.highBorderRadius, + ), + const SizedBox(width: 6), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + comment.user.fullName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Color.fromARGB(255, 102, 102, 102)), + ), + const SizedBox(height: 4), + DidvanText( + DateTimeUtils.momentGenerator(comment.createdAt), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + color: const Color.fromARGB(255, 0, 126, 167), + ), + ], + ), + const Spacer(), + DidvanIconButton( + size: 18, + gestureSize: 24, + icon: DidvanIcons.menu_light, + onPressed: () => _showCommentActions(comment), + ), + ], + ), + const SizedBox(height: 8), + if (isReply) DidvanText( - comment.user.fullName, - style: Theme.of(context).textTheme.bodyLarge, - ), - const Spacer(), - DidvanText( - DateTimeUtils.momentGenerator(comment.createdAt), + 'پاسخ به ${comment.toUser.fullName}', style: Theme.of(context).textTheme.bodySmall, color: Theme.of(context).colorScheme.caption, ), - const SizedBox(width: 4), - DidvanIconButton( - size: 18, - gestureSize: 24, - icon: DidvanIcons.menu_light, - onPressed: () => _showCommentActions(comment), - ), - ], - ), - const SizedBox(height: 8), - if (isReply) + const SizedBox(height: 8), DidvanText( - 'پاسخ به ${comment.toUser.fullName}', - style: Theme.of(context).textTheme.bodySmall, - color: Theme.of(context).colorScheme.caption, + comment.text, + color: const Color.fromARGB(255, 102, 102, 102), ), - const SizedBox(height: 8), - DidvanText(comment.text), - const SizedBox(height: 8), - if (comment.status == 2) - Row( - children: [ - Icon( - Icons.circle, - color: Theme.of(context) - .colorScheme - .secondary - .withValues(alpha: 0.3), - size: 18, - ), - const SizedBox(width: 4), - DidvanText( - 'در انتظار تایید', - color: Theme.of(context) - .colorScheme - .secondary - .withValues(alpha: 0.3), - ), - ], - ), - const SizedBox(height: 8), - if (_comment.status != 2) - Row( - children: [ - InkWrapper( - onPressed: () { - state.commentId = _comment.id; - state.replyingTo = comment.user; - state.showReplyBox = true; - state.update(); - widget.focusNode.requestFocus(); - }, - child: DidvanText( - 'پاسخ', - style: Theme.of(context).textTheme.bodyLarge, - color: Theme.of(context).colorScheme.primary, + const SizedBox(height: 8), + if (comment.status == 2) + Row( + children: [ + Icon( + Icons.circle, + color: Theme.of(context) + .colorScheme + .secondary + .withValues(alpha: 0.3), + size: 18, ), - ), - if (!isReply) const SizedBox(width: 20), - if (!isReply && comment.replies.isNotEmpty) - InkWrapper( - onPressed: () => setState( - () => _showSubComments = !_showSubComments, + const SizedBox(width: 4), + DidvanText( + 'در انتظار تایید', + color: Theme.of(context) + .colorScheme + .secondary + .withValues(alpha: 0.3), + ), + ], + ), + const SizedBox(height: 8), + if (_comment.status != 2) + Row( + children: [ + _FeedbackButtons( + likeCount: comment.feedback.like, + dislikeCount: comment.feedback.dislike, + likeValue: comment.liked, + dislikeValue: comment.disliked, + onFeedback: + (like, dislike, likeCount, dislikeCount) => + state.feedback( + id: _comment.id, + like: like, + dislike: dislike, + likeCount: likeCount, + dislikeCount: dislikeCount, + replyId: isReply ? comment.id : null, ), - child: Row( - children: [ - DidvanText( - 'پاسخ‌ها(${comment.replies.length})', - style: Theme.of(context).textTheme.bodyLarge, - color: Theme.of(context).colorScheme.primary, - ), - AnimatedRotation( - duration: DesignConfig.lowAnimationDuration, - turns: _showSubComments ? 0.5 : 0, - child: Icon( - DidvanIcons.angle_down_regular, + ), + const SizedBox( + width: 20, + ), + InkWrapper( + onPressed: () { + state.commentId = _comment.id; + state.replyingTo = comment.user; + state.showReplyBox = true; + state.update(); + widget.focusNode.requestFocus(); + }, + child: DidvanText( + 'پاسخ', + style: Theme.of(context).textTheme.bodySmall, + color: const Color.fromARGB(255, 102, 102, 102), + ), + ), + if (!isReply) const SizedBox(width: 20), + if (!isReply && comment.replies.isNotEmpty) + InkWrapper( + onPressed: () => setState( + () => _showSubComments = !_showSubComments, + ), + child: Row( + children: [ + DidvanText( + 'پاسخ‌ها(${comment.replies.length})', + style: + Theme.of(context).textTheme.bodyLarge, color: Theme.of(context).colorScheme.primary, ), - ), - ], + AnimatedRotation( + duration: DesignConfig.lowAnimationDuration, + turns: _showSubComments ? 0.5 : 0, + child: Icon( + DidvanIcons.angle_down_regular, + color: + Theme.of(context).colorScheme.primary, + ), + ), + ], + ), ), - ), - const Spacer(), - _FeedbackButtons( - likeCount: comment.feedback.like, - dislikeCount: comment.feedback.dislike, - likeValue: comment.liked, - dislikeValue: comment.disliked, - onFeedback: - (like, dislike, likeCount, dislikeCount) => - state.feedback( - id: _comment.id, - like: like, - dislike: dislike, - likeCount: likeCount, - dislikeCount: dislikeCount, - replyId: isReply ? comment.id : null, - ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - ], + ], + ), ), ); @@ -285,18 +312,10 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> { @override Widget build(BuildContext context) { return Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - DidvanText( - _likeCount.toString(), - style: Theme.of(context).textTheme.bodySmall, - color: Theme.of(context).colorScheme.caption, - ), - const SizedBox(width: 4), - DidvanIconButton( - icon: _likeValue ? DidvanIcons.like_solid : DidvanIcons.like_regular, - color: _likeValue ? Theme.of(context).colorScheme.primary : null, - gestureSize: 24, - onPressed: () { + GestureDetector( + onTap: () { setState(() { if (_likeValue) { _likeCount--; @@ -312,21 +331,76 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> { widget.onFeedback( _likeValue, _dislikeValue, _likeCount, _dislikeCount); }, - ), - const SizedBox(width: 16), - DidvanText( - _dislikeCount.toString(), - style: Theme.of(context).textTheme.bodySmall, - color: Theme.of(context).colorScheme.caption, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + transform: _likeValue + ? (Matrix4.identity()..scale(1.2)) + : Matrix4.identity(), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, + child: RotationTransition( + turns: + Tween(begin: 0.8, end: 1.0).animate(animation), + child: child, + ), + ); + }, + child: SvgPicture.asset( + _likeValue + ? 'lib/assets/icons/likefill.svg' + : 'lib/assets/icons/like.svg', + key: ValueKey(_likeValue), + color: + _likeValue ? const Color.fromARGB(255, 43, 178, 74) : null, + width: 16, + height: 16, + ), + ), + ), ), const SizedBox(width: 4), - DidvanIconButton( - icon: _dislikeValue - ? DidvanIcons.dislike_solid - : DidvanIcons.dislike_regular, - color: _dislikeValue ? Theme.of(context).colorScheme.secondary : null, - gestureSize: 24, - onPressed: () { + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: + _likeValue ? const Color.fromARGB(255, 43, 178, 74) : null, + fontWeight: _likeValue ? FontWeight.bold : FontWeight.normal, + ), + child: DidvanText( + 'پسندیدم', + color: _likeValue ? const Color.fromARGB(255, 43, 178, 74) : null, + style: const TextStyle(fontSize: 11), + ), + ), + const SizedBox( + width: 5, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 0.5), + end: const Offset(0, 0), + ).animate(animation), + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: DidvanText('(${_likeCount.toString()})', + key: ValueKey(_likeCount), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 11, + color: _likeValue + ? const Color.fromARGB(255, 43, 178, 74) + : null, + )), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: () { setState(() { if (_dislikeValue) { _dislikeCount--; @@ -342,7 +416,78 @@ class _FeedbackButtonsState extends State<_FeedbackButtons> { widget.onFeedback( _likeValue, _dislikeValue, _likeCount, _dislikeCount); }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + transform: _dislikeValue + ? (Matrix4.identity()..scale(1.2)) + : Matrix4.identity(), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, + child: RotationTransition( + turns: + Tween(begin: 0.8, end: 1.0).animate(animation), + child: child, + ), + ); + }, + child: SvgPicture.asset( + _dislikeValue + ? 'lib/assets/icons/dislike_fill.svg' + : 'lib/assets/icons/dislike.svg', + key: ValueKey(_dislikeValue), + color: _dislikeValue + ? const Color.fromARGB(255, 196, 18, 18) + : null, + width: 16, + height: 16, + ), + ), + ), ), + const SizedBox(width: 4), + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: _dislikeValue + ? const Color.fromARGB(255, 196, 18, 18) + : null, + fontWeight: _dislikeValue ? FontWeight.bold : FontWeight.normal, + ), + child: DidvanText( + 'نپسندیدم', + color: + _dislikeValue ? const Color.fromARGB(255, 196, 18, 18) : null, + fontSize: 11, + ), + ), + const SizedBox( + width: 5, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 0.5), + end: const Offset(0, 0), + ).animate(animation), + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: DidvanText( + '(${_dislikeCount.toString()})', + key: ValueKey(_dislikeCount), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 11, + ), + color: + _dislikeValue ? const Color.fromARGB(255, 196, 18, 18) : null, + ), + ), + const SizedBox(width: 4), ], ); } diff --git a/lib/views/home/media/podcast_tab_page.dart b/lib/views/home/media/podcast_tab_page.dart index f8f5431..e83c9d9 100644 --- a/lib/views/home/media/podcast_tab_page.dart +++ b/lib/views/home/media/podcast_tab_page.dart @@ -1,9 +1,16 @@ +import 'dart:async'; import 'package:didvan/models/requests/studio.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/home/media/widgets/featured_podcast_card.dart'; +import 'package:didvan/views/home/media/widgets/podcast_list_card.dart'; import 'package:didvan/views/podcasts/podcasts_state.dart'; -import 'package:didvan/views/widgets/overview/podcast.dart'; +import 'package:didvan/views/widgets/item_title.dart'; +// import 'package:didvan/views/widgets/overview/podcast.dart'; import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; class PodcastTabPage extends StatefulWidget { @@ -14,6 +21,10 @@ class PodcastTabPage extends StatefulWidget { } class _PodcastTabPageState extends State { + int _currentFeaturedIndex = 0; + Timer? _rotationTimer; + bool _isPlaying = false; + @override void initState() { super.initState(); @@ -23,6 +34,111 @@ class _PodcastTabPageState extends State { }); } + void _startRotation() { + _rotationTimer?.cancel(); + _rotationTimer = Timer.periodic(const Duration(seconds: 5), (timer) { + final state = context.read(); + if (state.studios.isNotEmpty && mounted && !_isPlaying) { + setState(() { + _currentFeaturedIndex = + (_currentFeaturedIndex + 1) % state.studios.length; + }); + } + }); + } + + void _onPlayStateChanged(bool isPlaying) { + setState(() { + _isPlaying = isPlaying; + }); + } + + void _showSortDialog() { + final state = context.read(); + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + builder: (context) { + return Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('تازه‌ترین‌ها'), + onTap: () { + state.selectedSortTypeIndex = 0; + state.getStudios(page: 1); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('قدیمی‌ترین‌ها'), + onTap: () { + state.selectedSortTypeIndex = 1; + state.getStudios(page: 1); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('پربازدیدترین‌ها'), + onTap: () { + state.selectedSortTypeIndex = 2; + state.getStudios(page: 1); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('پربحث‌ترین‌ها'), + onTap: () { + state.selectedSortTypeIndex = 3; + state.getStudios(page: 1); + Navigator.pop(context); + }, + ), + ], + ), + ); + }, + ); + } + + void _navigateToDetails(int index) { + final state = context.read(); + final item = state.studios[index]; + + Navigator.pushNamed( + context, + Routes.studioDetails, + arguments: { + 'id': item.id, + 'type': item.type, + 'onMarkChanged': (int id, bool value, [bool shouldUpdate = true]) { + context.read().changeMark(id, value, shouldUpdate); + }, + 'args': StudioRequestArgs( + page: state.page, + type: state.type, + search: state.search, + order: state.order, + asc: state.selectedSortTypeIndex == 1, + endDate: state.endDate?.split(' ').first, + startDate: state.startDate?.split(' ').first, + ), + }, + ); + } + + + @override + void dispose() { + _rotationTimer?.cancel(); + if (MediaService.isPlayingFromFeaturedCard && MediaService.audioPlayer.playing) { + MediaService.audioPlayer.pause(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { final state = context.watch(); @@ -33,31 +149,81 @@ class _PodcastTabPageState extends State { onNewSearch: () {}, ), enableEmptyState: state.studios.isEmpty, - placeholder: PodcastOverview.placeholder, builder: (context, state) { - return ListView.builder( - itemCount: state.studios.length, - itemBuilder: (context, index) { - final podcast = state.studios[index]; - return Padding( - padding: const EdgeInsets.only( - bottom: 8, - left: 16, - right: 16, - ), - child: PodcastOverview( - podcast: podcast, - onMarkChanged: state.changeMark, - studioRequestArgs: StudioRequestArgs( - page: state.page, - order: state.order, - search: state.search, - type: state.type, - asc: state.selectedSortTypeIndex == 1, + if (state.studios.isEmpty) { + return const SizedBox.shrink(); + } + + if (_rotationTimer == null || !_rotationTimer!.isActive) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _startRotation(); + }); + } + + return SingleChildScrollView( + child: Column( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: FeaturedPodcastCard( + key: ValueKey(state.studios[_currentFeaturedIndex].id), + podcast: state.studios[_currentFeaturedIndex], + onTap: () => _navigateToDetails(_currentFeaturedIndex), + onPlayStateChanged: _onPlayStateChanged, ), ), - ); - }, + + if (state.studios.length > 1) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const ItemTitle( + title: 'همه پادکست‌ها', + color: Color.fromARGB(255, 0, 53, 70), + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 18), + ), + Row( + children: [ + GestureDetector( + onTap: _showSortDialog, + child: Text( + state.orderString, + style: const TextStyle( + color: Color.fromARGB(255, 0, 53, 70)), + )), + IconButton( + onPressed: _showSortDialog, + icon: SvgPicture.asset( + 'lib/assets/icons/sort2.svg')), + ], + ), + ], + ), + ), + + if (state.studios.length > 1) + ListView.builder( + itemCount: state.studios.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final podcast = state.studios[index]; + + return PodcastListCard( + podcast: podcast, + onTap: () => _navigateToDetails(index), + ); + }, + ), + ], + ), ); }, onRetry: () => context.read().getStudios(page: 1), diff --git a/lib/views/home/media/video_details_page.dart b/lib/views/home/media/video_details_page.dart index b7d7be9..51cdcd2 100644 --- a/lib/views/home/media/video_details_page.dart +++ b/lib/views/home/media/video_details_page.dart @@ -2,9 +2,12 @@ import 'package:chewie/chewie.dart'; import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/providers/user.dart'; +import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/views/comments/comments.dart'; import 'package:didvan/views/comments/comments_state.dart'; @@ -32,15 +35,47 @@ class VideoDetailsPage extends StatefulWidget { State createState() => _VideoDetailsPageState(); } -class _VideoDetailsPageState extends State { +class _VideoDetailsPageState extends State + with TickerProviderStateMixin { int _currentlyPlayingId = 0; VideoPlayerController? _videoPlayerController; ChewieController? _chewieController; bool _isDescriptionExpanded = false; + final _focusNode = FocusNode(); + + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + final GlobalKey _relatedContentKey = + GlobalKey(); + final GlobalKey _commentsKey = + GlobalKey(); @override void initState() { super.initState(); + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeIn, + ), + ); + + _slideAnimation = + Tween(begin: const Offset(0, 0.2), end: Offset.zero).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + ), + ); + final state = context.read(); state.args = widget.pageData['args']; @@ -52,6 +87,7 @@ class _VideoDetailsPageState extends State { Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { state.getRelatedContents(); + _animationController.forward(); } }); } @@ -167,48 +203,59 @@ class _VideoDetailsPageState extends State { ), )), body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: 16 / 9, - child: Stack( - children: [ - (_chewieController != null && - _chewieController!.videoPlayerController - .value.isInitialized) - ? Chewie(controller: _chewieController!) - : Center( - child: Image.asset( - Assets.loadingAnimation, - width: 100, - height: 100, + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'video-${state.studio.id}', + child: AspectRatio( + aspectRatio: 16 / 9, + child: Stack( + children: [ + (_chewieController != null && + _chewieController! + .videoPlayerController + .value + .isInitialized) + ? Chewie(controller: _chewieController!) + : Center( + child: Image.asset( + Assets.loadingAnimation, + width: 100, + height: 100, + ), + ), + Positioned( + top: 1, + left: 1, + child: BookmarkButton( + value: state.studio.marked, + onMarkChanged: (value) { + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged']( + state.studio.id, value); + } + }, + gestureSize: 35, + type: 'video', + itemId: state.studio.id, ), ), - Positioned( - top: 1, - left: 1, - child: BookmarkButton( - value: state.studio.marked, - onMarkChanged: (value) { - if (widget.pageData['onMarkChanged'] != - null) { - widget.pageData['onMarkChanged']( - state.studio.id, value); - } - }, - gestureSize: 35, - type: 'video', - itemId: state.studio.id, + ], ), ), - ], - ), + ), + _buildDescriptionSection(state), + _buildRelatedContentSection(state), + _buildCommentsSection(state), + ], ), - _buildDescriptionSection(state), - _buildRelatedContentSection(state), - _buildCommentsSection(state), - ], + ), ), ), ), @@ -220,180 +267,236 @@ class _VideoDetailsPageState extends State { } Widget _buildDescriptionSection(StudioDetailsState state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - state.studio.title, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 0, 53, 70)), - ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: _isDescriptionExpanded ? double.infinity : 100.0, + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: KeyedSubtree( + key: ValueKey('description-${state.studio.id}'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + state.studio.title, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 0, 53, 70)), + ), ), - child: Html( - key: ValueKey(state.studio.id), - data: state.studio.description, - onAnchorTap: (href, _, __) => launchUrlString(href!), - style: { - '*': Style( - direction: TextDirection.rtl, - textAlign: TextAlign.right, - lineHeight: LineHeight.percent(135), - margin: const Margins(), - padding: HtmlPaddings.zero, - color: const Color.fromARGB(255, 102, 102, 102), - fontWeight: FontWeight.normal, - ), - }, - ), - ), - ), - InkWell( - onTap: () { - setState(() { - _isDescriptionExpanded = !_isDescriptionExpanded; - }); - }, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + Stack( + alignment: Alignment.bottomCenter, children: [ - SvgPicture.asset( - _isDescriptionExpanded - ? 'lib/assets/icons/arrow-up2.svg' - : 'lib/assets/icons/arrow-down.svg', - color: Theme.of(context).primaryColor, - height: 20, + AnimatedSize( + duration: const Duration(milliseconds: 300), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular( + _isDescriptionExpanded ? 0 : 16.0), + ), + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: _isDescriptionExpanded + ? double.infinity + : 100.0, + ), + child: Html( + key: ValueKey(state.studio.id), + data: state.studio.description, + onAnchorTap: (href, _, __) => + launchUrlString(href!), + style: { + '*': Style( + direction: TextDirection.rtl, + textAlign: TextAlign.right, + lineHeight: LineHeight.percent(135), + margin: const Margins(), + padding: HtmlPaddings.zero, + color: const Color.fromARGB( + 255, 102, 102, 102), + fontWeight: FontWeight.normal, + ), + }, + ), + ), + ), ), - const SizedBox(width: 4), - Text( - _isDescriptionExpanded ? 'کمتر' : 'بیشتر', - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.normal, + Positioned( + bottom: 0, + left: 0, + right: 0, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _isDescriptionExpanded ? 0.0 : 1.0, + child: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(16.0), + ), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withOpacity(0.0), + Theme.of(context).colorScheme.surface, + ], + stops: const [0.0, 0.9], + ), + ), + ), ), ), ], ), - ), - ), - if (state.studio.tags.isNotEmpty) const SizedBox(height: 16), - if (state.studio.tags.isNotEmpty) - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (var i = 0; i < state.studio.tags.length; i++) - TagItem( - tag: state.studio.tags[i], - onMarkChanged: (id, value) { - if (widget.pageData['onMarkChanged'] != null) { - widget.pageData['onMarkChanged'](id, value); - } - }, - type: 'video', + InkWell( + onTap: () { + setState(() { + _isDescriptionExpanded = !_isDescriptionExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + _isDescriptionExpanded + ? 'lib/assets/icons/arrow-up2.svg' + : 'lib/assets/icons/arrow-down.svg', + color: Theme.of(context).primaryColor, + height: 20, + ), + ], ), - ], - ), - ], - ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - if (state.nextStudio != null && - state.alongSideState == AppState.idle) - StudioPreview( - isNext: true, - studio: state.nextStudio!, + ), + ), + if (state.studio.tags.isNotEmpty) const SizedBox(height: 16), + if (state.studio.tags.isNotEmpty) + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; i < state.studio.tags.length; i++) + TagItem( + tag: state.studio.tags[i], + onMarkChanged: (id, value) { + if (widget.pageData['onMarkChanged'] != null) { + widget.pageData['onMarkChanged'](id, value); + } + }, + type: 'video', + ), + ], + ), + ], ), - if (state.alongSideState == AppState.busy) - StudioPreview.placeHolder, - if (state.prevStudio != null && - state.alongSideState == AppState.idle) - StudioPreview( - isNext: false, - studio: state.prevStudio!, - ), - if (state.alongSideState == AppState.busy) - StudioPreview.placeHolder, - const SizedBox(), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + if (state.nextStudio != null && + state.alongSideState == AppState.idle) + StudioPreview( + isNext: true, + studio: state.nextStudio!, + ), + if (state.alongSideState == AppState.busy) + StudioPreview.placeHolder, + if (state.prevStudio != null && + state.alongSideState == AppState.idle) + StudioPreview( + isNext: false, + studio: state.prevStudio!, + ), + if (state.alongSideState == AppState.busy) + StudioPreview.placeHolder, + const SizedBox(), + ], + ), ], ), - ], + ), ); } Widget _buildCommentsSection(StudioDetailsState state) { - return Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Color(0xFF059669), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: const DidvanText( - 'نظرات', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - SizedBox( - height: 400, - child: ChangeNotifierProvider( - create: (context) => CommentsState(), - child: Comments( - pageData: { - 'id': state.studio.id, - 'type': 'studio', - 'title': state.studio.title, - 'onCommentsChanged': state.onCommentsChanged, - 'isPage': false, + return ChangeNotifierProvider( + create: (context) => CommentsState() + ..itemId = state.studio.id + ..type = 'studio' + ..onCommentsChanged = state.onCommentsChanged + ..getComments(), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, userProvider, child) { + final user = userProvider.user; + final hasProfileImage = + user?.photo != null && user!.photo!.isNotEmpty; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: + hasProfileImage ? NetworkImage(user.photo!) : null, + child: !hasProfileImage + ? const Icon( + DidvanIcons.avatar_light, + size: 50, + color: Colors.black, + ) + : null, + ), + const SizedBox(width: 8), + Expanded( + child: CommentMessageBox(focusNode: _focusNode), + ), + ], + ); }, ), - ), + const SizedBox(height: 16), + const SizedBox( + width: double.infinity, + child: DidvanText( + 'نظرات کاربران:', + style: TextStyle( + color: Color.fromARGB(255, 0, 53, 70), + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 400, + child: Comments( + key: _commentsKey, + pageData: const {'isPage': false}, + ), + ), + ], ), - ], + ), ), ); } @@ -415,7 +518,7 @@ class _VideoDetailsPageState extends State { child: Text( "مطالب مرتبط:", style: TextStyle( - fontSize: 16, + fontSize: 18, color: Color.fromARGB(255, 0, 53, 70), fontWeight: FontWeight.bold), ), @@ -424,37 +527,81 @@ class _VideoDetailsPageState extends State { padding: const EdgeInsets.all(8), child: Builder( builder: (context) { - debugPrint("Building related content section:"); - debugPrint( - " - relatedContents.length: ${state.studio.relatedContents.length}"); - debugPrint( - " - relatedContentsIsEmpty: ${state.studio.relatedContentsIsEmpty}"); - debugPrint(" - tags.length: ${state.studio.tags.length}"); - if (state.studio.relatedContents.isNotEmpty) { - debugPrint( - " - Showing ${state.studio.relatedContents.length} related items"); - return Column( - children: [ - ...state.studio.relatedContents - .map((item) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MultitypeOverview( - item: item, - onMarkChanged: (id, value) { - if (widget.pageData['onMarkChanged'] != - null) { - widget.pageData['onMarkChanged']( - id, value); - } - }, - ), - )) - .toList(), - ], + return AnimatedList( + key: _relatedContentKey, + initialItemCount: state.studio.relatedContents.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index, animation) { + final item = state.studio.relatedContents[index]; + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.2), + end: Offset.zero, + ).animate(animation), + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + String routeName; + Map arguments; + + if (item.type == 'video') { + routeName = Routes.videoDetails; + arguments = { + 'id': item.id, + 'type': item.type, + }; + } else if (item.type == 'podcast') { + routeName = Routes.studioDetails; + arguments = { + 'id': item.id, + 'type': item.type, + }; + } else if (item.type == 'news') { + routeName = Routes.newsDetails; + arguments = { + 'id': item.id, + }; + } else if (item.type == 'radar') { + routeName = Routes.radarDetails; + arguments = { + 'id': item.id, + }; + } else { + routeName = Routes.studioDetails; + arguments = { + 'id': item.id, + 'type': item.type, + }; + } + + Navigator.pushNamed( + context, + routeName, + arguments: arguments, + ); + }, + child: MultitypeOverview( + item: item, + onMarkChanged: (id, value) { + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged'](id, value); + } + }, + ), + ), + ), + ), + ); + }, ); } else if (state.studio.relatedContentsIsEmpty) { - debugPrint(" - Showing empty message"); return const Padding( padding: EdgeInsets.all(32.0), child: Center( @@ -468,7 +615,6 @@ class _VideoDetailsPageState extends State { ), ); } else { - debugPrint(" - Showing placeholders (loading state)"); return Column( children: [ const Padding( @@ -500,8 +646,10 @@ class _VideoDetailsPageState extends State { @override void dispose() { + _animationController.dispose(); _videoPlayerController?.dispose(); _chewieController?.dispose(); + _focusNode.dispose(); super.dispose(); } } diff --git a/lib/views/home/media/widgets/audio_waveform_progress.dart b/lib/views/home/media/widgets/audio_waveform_progress.dart new file mode 100644 index 0000000..b0fabb2 --- /dev/null +++ b/lib/views/home/media/widgets/audio_waveform_progress.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class AudioWaveformProgress extends StatelessWidget { + final double progress; + final bool isActive; + final ValueChanged? onChanged; + + const AudioWaveformProgress({ + super.key, + required this.progress, + this.isActive = true, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: onChanged != null + ? (details) { + final box = context.findRenderObject() as RenderBox; + final localPosition = details.localPosition; + final value = localPosition.dx / box.size.width; + onChanged?.call(value.clamp(0.0, 1.0)); + } + : null, + onHorizontalDragUpdate: onChanged != null + ? (details) { + final box = context.findRenderObject() as RenderBox; + final localPosition = details.localPosition; + final value = localPosition.dx / box.size.width; + onChanged?.call(value.clamp(0.0, 1.0)); + } + : null, + child: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: CustomPaint( + painter: WaveformPainter( + progress: progress.clamp(0.0, 1.0), + isActive: isActive, + ), + size: Size.infinite, + ), + ), + ); + } +} + +class WaveformPainter extends CustomPainter { + final double progress; + final bool isActive; + + WaveformPainter({ + required this.progress, + required this.isActive, + }); + + @override + void paint(Canvas canvas, Size size) { + final random = math.Random(42); // استفاده از seed ثابت برای consistency + const barCount = 60; // تعداد میله‌ها + final barWidth = (size.width / barCount) * 0.45; // عرض هر میله + final spacing = size.width / barCount; + + final inactivePaint = Paint() + ..color = const Color.fromARGB(255, 102, 102, 102) + ..strokeWidth = barWidth + ..strokeCap = StrokeCap.round; + + final activePaint = Paint() + ..color = const Color.fromARGB(255, 0, 126, 167) + ..strokeWidth = barWidth + ..strokeCap = StrokeCap.round; + + // رسم میله‌های موج صدا + for (int i = 0; i < barCount; i++) { + final x = i * spacing + spacing / 2; + + // ارتفاع تصادفی برای هر میله (شبیه‌سازی موج صدا) + final randomHeight = random.nextDouble(); + final minHeight = size.height * 0.2; + final maxHeight = size.height * 0.9; + final barHeight = minHeight + (randomHeight * (maxHeight - minHeight)); + + final y1 = (size.height - barHeight) / 2; + final y2 = y1 + barHeight; + + // تعیین رنگ بر اساس progress + final barProgress = i / barCount; + final paint = barProgress <= progress && isActive ? activePaint : inactivePaint; + + canvas.drawLine( + Offset(x, y1), + Offset(x, y2), + paint, + ); + } + + // // رسم thumb (دایره کوچک) در محل progress + // if (isActive && progress > 0) { + // final thumbX = progress * size.width; + // final thumbPaint = Paint() + // ..color = const Color(0xFF3B82F6) + // ..style = PaintingStyle.fill; + + // canvas.drawCircle( + // Offset(thumbX, size.height / 2), + // 6, + // thumbPaint, + // ); + + // // سایه برای thumb + // final shadowPaint = Paint() + // ..color = const Color(0xFF3B82F6).withOpacity(0.3) + // ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4); + + // canvas.drawCircle( + // Offset(thumbX, size.height / 2), + // 8, + // shadowPaint, + // ); + // } + } + + @override + bool shouldRepaint(WaveformPainter oldDelegate) { + return oldDelegate.progress != progress || oldDelegate.isActive != isActive; + } +} diff --git a/lib/views/home/media/widgets/featured_podcast_card.dart b/lib/views/home/media/widgets/featured_podcast_card.dart new file mode 100644 index 0000000..ba2948e --- /dev/null +++ b/lib/views/home/media/widgets/featured_podcast_card.dart @@ -0,0 +1,376 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class FeaturedPodcastCard extends StatefulWidget { + final OverviewData podcast; + final VoidCallback onTap; + final Function(bool)? onPlayStateChanged; + + const FeaturedPodcastCard({ + super.key, + required this.podcast, + required this.onTap, + this.onPlayStateChanged, + }); + + @override + State createState() => _FeaturedPodcastCardState(); +} + +class _FeaturedPodcastCardState extends State + with WidgetsBindingObserver { + bool _isPlaying = false; + bool _isLoading = false; + double _progress = 0.0; + Duration _currentPosition = Duration.zero; + Duration _totalDuration = Duration.zero; + + var _subscriptions = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + if (widget.podcast.duration != null && widget.podcast.duration! > 0) { + _totalDuration = Duration(seconds: widget.podcast.duration!); + } + + _checkCurrentPlayingState(); + + _setupAudioListener(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive) { + if (_isPlaying && MediaService.currentPodcast?.id == widget.podcast.id) { + MediaService.audioPlayer.pause(); + } + } + } + + void _checkCurrentPlayingState() { + if (MediaService.currentPodcast?.id == widget.podcast.id) { + setState(() { + _isPlaying = MediaService.audioPlayer.playing; + }); + } + } + + void _setupAudioListener() { + _subscriptions + .add(MediaService.audioPlayer.positionStream.listen((position) { + if (mounted && MediaService.currentPodcast?.id == widget.podcast.id) { + setState(() { + _currentPosition = position; + if (_totalDuration.inSeconds > 0) { + _progress = position.inSeconds / _totalDuration.inSeconds; + } + }); + } + })); + + _subscriptions + .add(MediaService.audioPlayer.durationStream.listen((duration) { + if (mounted && + duration != null && + MediaService.currentPodcast?.id == widget.podcast.id) { + setState(() { + _totalDuration = duration; + }); + } + })); + + _subscriptions.add(MediaService.audioPlayer.playingStream.listen((playing) { + if (mounted && MediaService.currentPodcast?.id == widget.podcast.id) { + setState(() { + _isPlaying = playing; + }); + widget.onPlayStateChanged?.call(playing); + } + })); + } + + Future _togglePlayPause() async { + if (_isLoading) return; + + try { + final isCurrentPodcast = + MediaService.currentPodcast?.id == widget.podcast.id; + + if (!isCurrentPodcast) { + setState(() { + _isLoading = true; + }); + + if (widget.podcast.link == null || widget.podcast.link!.isEmpty) { + throw Exception('لینک پادکست معتبر نیست'); + } + + MediaService.currentPodcast = StudioDetailsData( + id: widget.podcast.id, + title: widget.podcast.title, + description: widget.podcast.description, + image: widget.podcast.image, + link: widget.podcast.link ?? '', + type: widget.podcast.type, + duration: widget.podcast.duration ?? 0, + iframe: widget.podcast.iframe, + createdAt: widget.podcast.createdAt, + order: 0, + marked: false, + comments: 0, + tags: [], + ); + + MediaService.isPlayingFromFeaturedCard = true; + + await MediaService.handleAudioPlayback( + audioSource: widget.podcast.link!, + id: widget.podcast.id, + isVoiceMessage: false, + isNetworkAudio: true, + ); + + if (mounted) { + setState(() { + _isPlaying = true; + _isLoading = false; + }); + widget.onPlayStateChanged?.call(true); + } + } else { + if (_isPlaying) { + await MediaService.audioPlayer.pause(); + if (mounted) { + setState(() { + _isPlaying = false; + }); + widget.onPlayStateChanged?.call(false); + } + } else { + await MediaService.audioPlayer.play(); + if (mounted) { + setState(() { + _isPlaying = true; + }); + widget.onPlayStateChanged?.call(true); + } + } + } + } catch (e) { + debugPrint('Error playing podcast: $e'); + if (mounted) { + setState(() { + _isLoading = false; + _isPlaying = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('خطا در پخش پادکست: ${e.toString()}'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } + } + + Future _seekForward() async { + final newPosition = _currentPosition + const Duration(seconds: 10); + if (newPosition < _totalDuration) { + await MediaService.audioPlayer.seek(newPosition); + } + } + + Future _seekBackward() async { + final newPosition = _currentPosition - const Duration(seconds: 5); + if (newPosition > Duration.zero) { + await MediaService.audioPlayer.seek(newPosition); + } else { + await MediaService.audioPlayer.seek(Duration.zero); + } + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$minutes:$seconds'; + } + + @override + Widget build(BuildContext context) { + final bool isCurrentlyPlaying = + MediaService.currentPodcast?.id == widget.podcast.id; + + return Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFEBEBEB), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 2), + child: GestureDetector( + onTap: widget.onTap, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: SkeletonImage( + imageUrl: widget.podcast.image, + width: double.infinity, + height: 200, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DidvanText( + widget.podcast.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 16), + Row( + children: [ + SizedBox( + width: 45, + child: Text( + _formatDuration(_totalDuration), + style: const TextStyle( + fontSize: 12, + color: Color.fromARGB(255, 102, 102, 102)), + textAlign: TextAlign.left, + ), + ), + const SizedBox(width: 8), + Expanded( + child: AudioWaveformProgress( + progress: isCurrentlyPlaying + ? _progress.clamp(0.0, 1.0) + : 0.0, + isActive: isCurrentlyPlaying, + onChanged: isCurrentlyPlaying + ? (value) { + final newPosition = Duration( + seconds: (value * _totalDuration.inSeconds) + .toInt(), + ); + MediaService.audioPlayer.seek(newPosition); + } + : null, + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 45, + child: Text( + _formatDuration(_currentPosition), + style: const TextStyle( + fontSize: 12, + color: Color.fromARGB(255, 102, 102, 102)), + textAlign: TextAlign.right, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: isCurrentlyPlaying ? _seekForward : null, + icon: SvgPicture.asset( + 'lib/assets/icons/forward-10-seconds.svg', + width: 30, + height: 30, + ), + ), + const SizedBox(width: 15), + GestureDetector( + onTap: _togglePlayPause, + child: _isLoading + ? const SizedBox( + width: 50, + height: 50, + child: Center( + child: CircularProgressIndicator( + color: Color(0xFF3B82F6), + strokeWidth: 3, + ), + ), + ) + : AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + child: SvgPicture.asset( + isCurrentlyPlaying && _isPlaying + ? 'lib/assets/icons/pause-circle.svg' + : 'lib/assets/icons/play.svg', + key: ValueKey( + isCurrentlyPlaying && _isPlaying), + width: + isCurrentlyPlaying && _isPlaying ? 55 : 50, + height: + isCurrentlyPlaying && _isPlaying ? 55 : 50, + ), + ), + ), + const SizedBox(width: 15), + IconButton( + onPressed: isCurrentlyPlaying ? _seekBackward : null, + icon: SvgPicture.asset( + 'lib/assets/icons/backward-5-seconds.svg', + width: 30, + height: 30, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + + for (var subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + super.dispose(); + } +} diff --git a/lib/views/home/media/widgets/podcast_list_card.dart b/lib/views/home/media/widgets/podcast_list_card.dart new file mode 100644 index 0000000..c076b6d --- /dev/null +++ b/lib/views/home/media/widgets/podcast_list_card.dart @@ -0,0 +1,138 @@ +import 'package:didvan/models/overview_data.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class PodcastListCard extends StatelessWidget { + final OverviewData podcast; + final VoidCallback onTap; + + const PodcastListCard({ + super.key, + required this.podcast, + required this.onTap, + }); + + String _formatDuration(int? duration) { + if (duration == null) return ''; + final minutes = duration ~/ 60; + final seconds = duration % 60; + return '”${seconds.toString().padLeft(2, '0')}:’${minutes.toString().padLeft(2, '0')}'; + } + + String _formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + return date.toPersianDateStr(); + } catch (e) { + return dateStr; + } + } + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + height: 120, + margin: const EdgeInsets.only( + bottom: 8, + left: 16, + right: 16, + ), + decoration: BoxDecoration( + color: const Color.fromRGBO(235, 255, 255, 255), + borderRadius: BorderRadius.circular(20), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: AspectRatio( + aspectRatio: 1.42, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SkeletonImage( + imageUrl: podcast.image, + width: double.infinity, + height: double.infinity, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 3, 12, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + DidvanText( + podcast.title, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset('lib/assets/icons/calendar.svg'), + const SizedBox(width: 4), + DidvanText( + _formatDate(podcast.createdAt), + style: textTheme.bodySmall?.copyWith( + color: const Color.fromARGB(255, 102, 102, 102), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox(height: 7), + Row( + children: [ + SvgPicture.asset( + 'lib/assets/icons/clock.svg', + color: const Color.fromARGB(255, 102, 102, 102), + ), + const SizedBox(width: 4), + DidvanText( + _formatDuration(podcast.duration).toPersianDigit(), + style: textTheme.bodySmall?.copyWith( + color: const Color.fromARGB(255, 102, 102, 102), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + Divider( + color: Colors.grey[300], + thickness: 2, + indent: 16, + endIndent: 16, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/podcasts/studio_details/studio_details.mobile.dart b/lib/views/podcasts/studio_details/studio_details.mobile.dart index ceaa3d2..94ac04d 100644 --- a/lib/views/podcasts/studio_details/studio_details.mobile.dart +++ b/lib/views/podcasts/studio_details/studio_details.mobile.dart @@ -157,7 +157,7 @@ class _StudioDetailsState extends State { value: state.studio.marked, onMarkChanged: (value) { widget.pageData['onMarkChanged']( - state.studio.id, value); + state.studio.id, value, true); }, gestureSize: 48, ), diff --git a/lib/views/widgets/audio/player_navbar.dart b/lib/views/widgets/audio/player_navbar.dart index 7916640..8978597 100644 --- a/lib/views/widgets/audio/player_navbar.dart +++ b/lib/views/widgets/audio/player_navbar.dart @@ -40,71 +40,115 @@ class _PlayerNavBarState extends State { Widget build(BuildContext context) { return StreamBuilder( stream: MediaService.audioPlayer.playingStream, - 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) { + builder: (context, isPlaying) { + return GestureDetector( + onTap: () { + if (MediaService.isPlayingFromFeaturedCard) { + return; + } + + (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) && + !MediaService.isPlayingFromFeaturedCard + ? 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 (MediaService.isPlayingFromFeaturedCard) { + return const SizedBox(); + } + + 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, vertical: 16) - .copyWith(bottom: widget.inHome ? 72 + 16 : 16), + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ DidvanIconButton( icon: DidvanIcons.close_regular, @@ -114,137 +158,107 @@ class _PlayerNavBarState extends State { 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: Center( - child: Padding( - padding: const EdgeInsets.only(left: 48), - child: SpinKitThreeBounce( - size: 18, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + MediaService.currentPodcast!.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, color: DesignConfig.isDark - ? Theme.of(context).colorScheme.title + ? 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, + ), + ) ], ), ); - } - 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, - ), - ) - ], - ), - ); - }), + }), + ), ), - ), - ), + ); + }, ); } diff --git a/lib/views/widgets/overview/multitype.dart b/lib/views/widgets/overview/multitype.dart index 54ae5f0..e51e4b6 100644 --- a/lib/views/widgets/overview/multitype.dart +++ b/lib/views/widgets/overview/multitype.dart @@ -1,12 +1,10 @@ import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; -import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/widgets/bookmark_button.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; @@ -88,14 +86,15 @@ class MultitypeOverview extends StatelessWidget { children: [ Stack( children: [ - SkeletonImage(imageUrl: item.image, height: 80, width: 80), + SkeletonImage(imageUrl: item.image, height: 100, width: 140), Container( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, borderRadius: const BorderRadius.horizontal( - left: Radius.circular(20), + left: Radius.circular(16), + right: Radius.circular(16), ), ), child: SvgPicture.asset( @@ -127,10 +126,7 @@ class MultitypeOverview extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( - DidvanIcons.calendar_day_light, - size: 16, - ), + SvgPicture.asset('lib/assets/icons/calendar.svg'), const SizedBox(width: 4), DidvanText( DateTime.parse(item.createdAt) @@ -140,26 +136,26 @@ class MultitypeOverview extends StatelessWidget { ], ), const Spacer(), - if ((item.timeToRead ?? item.duration) != null) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon( - DidvanIcons.timer_light, - size: 16, - ), - const SizedBox(width: 4), - DidvanText( - item.timeToRead != null - ? '${item.timeToRead} دقیقه' - : DateTimeUtils.normalizeTimeDuration( - Duration(seconds: item.duration!), - ), - style: - Theme.of(context).textTheme.labelSmall, - ), - ], - ), + // if ((item.timeToRead ?? item.duration) != null) + // Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // const Icon( + // DidvanIcons.timer_light, + // size: 16, + // ), + // const SizedBox(width: 4), + // DidvanText( + // item.timeToRead != null + // ? '${item.timeToRead} دقیقه' + // : DateTimeUtils.normalizeTimeDuration( + // Duration(seconds: item.duration!), + // ), + // style: + // Theme.of(context).textTheme.labelSmall, + // ), + // ], + // ), ], ), ),