diff --git a/lib/assets/icons/timer-pause.svg b/lib/assets/icons/timer-pause.svg new file mode 100644 index 0000000..7d37c7a --- /dev/null +++ b/lib/assets/icons/timer-pause.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/assets/icons/video-circle.svg b/lib/assets/icons/video-circle.svg new file mode 100644 index 0000000..3311a5e --- /dev/null +++ b/lib/assets/icons/video-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 7664f52..604a8ee 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -226,7 +226,7 @@ class RequestHelper { static String aiAChat(int id) => '$baseUrl/ai/chat/$id/v2'; static String aiChatId() => '$baseUrl/ai/chat/id'; static String aiDeleteChats() => '$baseUrl/ai/chat'; - static String aiChangeChats(int id) => '$baseUrl/ai/chat/$id/title'; + static String aiChangeChats(int id) => '$baseUrl/ai/chat/$id/title'; static String deleteChat(int id) => '$baseUrl/ai/chat/$id'; static String deleteMessage(int chatId, int messageId) => '$baseUrl/ai/chat/$chatId/message/$messageId'; diff --git a/lib/views/comments/widgets/comment.dart b/lib/views/comments/widgets/comment.dart index 1dc0d20..2bb3ea7 100644 --- a/lib/views/comments/widgets/comment.dart +++ b/lib/views/comments/widgets/comment.dart @@ -9,7 +9,6 @@ import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/comments/comments_state.dart'; import 'package:didvan/views/widgets/menu_item.dart'; -import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/ink_wrapper.dart'; @@ -34,8 +33,6 @@ class Comment extends StatefulWidget { class CommentState extends State { late final CommentsState state; - bool _showSubComments = false; - CommentData get _comment => widget.comment; @override @@ -51,191 +48,147 @@ class CommentState extends State { _commentBuilder(comment: _comment), if (_comment.replies.isNotEmpty) const SizedBox(height: 16), for (var i = 0; i < _comment.replies.length; i++) - AnimatedVisibility( - duration: DesignConfig.lowAnimationDuration, - isVisible: _showSubComments, - child: _commentBuilder( - isReply: true, - comment: _comment.replies[i], - ), + _commentBuilder( + isReply: true, + comment: _comment.replies[i], ), ], ); } Widget _commentBuilder({required comment, bool isReply = false}) => Container( + margin: EdgeInsets.only(right: isReply ? 24.0 : 0.0), decoration: BoxDecoration( border: Border( right: isReply - ? BorderSide(color: Theme.of(context).colorScheme.caption) + ? BorderSide( + color: Theme.of(context).colorScheme.caption, + width: 3.0, + ) : BorderSide.none, ), ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 0), - child: Row( + padding: EdgeInsets.only(right: isReply ? 16.0 : 0.0), + child: Column( 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), - ), - ], + Row( + children: [ + if (comment.user.photo == null) + const Icon( + DidvanIcons.avatar_light, + size: 50, ), - const SizedBox(height: 8), - if (isReply) + 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.toUser.fullName}', - style: Theme.of(context).textTheme.bodySmall, - color: Theme.of(context).colorScheme.caption, + 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), + DidvanText( + comment.text, + color: const Color.fromARGB(255, 102, 102, 102), + ), + 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: [ + _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, + ), + ), + 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), ), - const SizedBox(height: 8), - DidvanText( - comment.text, - color: const Color.fromARGB(255, 102, 102, 102), ), - 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: [ - _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, - ), - ), - 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, - ), - ), - ], - ), - ), - ], - ), ], ), - ), ], ), ), ); - Future _showCommentActions(comment) async { ActionSheetUtils(context).showBottomSheet( data: ActionSheetData( diff --git a/lib/views/home/media/media_page.dart b/lib/views/home/media/media_page.dart index c037dfe..4aff354 100644 --- a/lib/views/home/media/media_page.dart +++ b/lib/views/home/media/media_page.dart @@ -5,6 +5,8 @@ import 'package:didvan/views/home/main/widgets/banner.dart'; import 'package:didvan/views/widgets/home_app_bar.dart'; import 'package:didvan/views/widgets/custom_media_tab_bar.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:didvan/views/podcasts/podcasts_state.dart'; class MediaPage extends StatefulWidget { const MediaPage({super.key}); @@ -84,8 +86,16 @@ class _MediaPageState extends State { }); }, children: [ - const PodcastTabPage(key: ValueKey('PodcastTabPage')), - const VideoCastTabPage(key: ValueKey('VideoCastTabPage')), + ChangeNotifierProvider( + create: (_) => PodcastsState(), + child: const PodcastTabPage(key: ValueKey('PodcastTabPage')), + ), + ChangeNotifierProvider( + create: (_) => PodcastsState(), + child: + const VideoCastTabPage(key: ValueKey('VideoCastTabPage')), + ), + const SingleChildScrollView( key: ValueKey('MainPageBanner'), child: Padding( diff --git a/lib/views/home/media/video_details_page.dart b/lib/views/home/media/video_details_page.dart index 51cdcd2..d5afd35 100644 --- a/lib/views/home/media/video_details_page.dart +++ b/lib/views/home/media/video_details_page.dart @@ -289,93 +289,127 @@ class _VideoDetailsPageState extends State color: Color.fromARGB(255, 0, 53, 70)), ), ), - Stack( - alignment: Alignment.bottomCenter, - children: [ - 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, - ), - }, - ), - ), - ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular( + _isDescriptionExpanded ? 0 : 16.0), ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: AnimatedOpacity( + boxShadow: _isDescriptionExpanded + ? null + : [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8.0, + spreadRadius: -2.0, + offset: const Offset(0, 6), + ), + ], + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + AnimatedSize( 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], + child: ClipRRect( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular( + _isDescriptionExpanded ? 0 : 16.0), + top: Radius.circular( + _isDescriptionExpanded ? 0 : 16.0), + ), + child: Container( + color: Theme.of(context).colorScheme.surface, + 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, + ), + }, + ), ), ), ), ), - ), - ], - ), - 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, + 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), + top: 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], + ), + ), + ), ), - ], + ), + ], + ), + ), + Transform.translate( + offset: const Offset(0, -14.0), + child: InkWell( + onTap: () { + setState(() { + _isDescriptionExpanded = !_isDescriptionExpanded; + }); + }, + child: Padding( + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(4.0), + decoration: const BoxDecoration( + color: Color.fromARGB(255, 230, 243, 250), + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + _isDescriptionExpanded + ? 'lib/assets/icons/arrow-up2.svg' + : 'lib/assets/icons/arrow-down.svg', + color: Theme.of(context).primaryColor, + height: 25, + ), + ), + ], + ), ), ), ), @@ -449,7 +483,7 @@ class _VideoDetailsPageState extends State builder: (context, userProvider, child) { final user = userProvider.user; final hasProfileImage = - user?.photo != null && user!.photo!.isNotEmpty; + user.photo != null && user.photo!.isNotEmpty; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/views/home/media/widgets/audio_waveform_progress.dart b/lib/views/home/media/widgets/audio_waveform_progress.dart index b0fabb2..46ed164 100644 --- a/lib/views/home/media/widgets/audio_waveform_progress.dart +++ b/lib/views/home/media/widgets/audio_waveform_progress.dart @@ -89,7 +89,8 @@ class WaveformPainter extends CustomPainter { final y2 = y1 + barHeight; // تعیین رنگ بر اساس progress - final barProgress = i / barCount; + // استفاده از (i + 1) تا میله اول از 1/60 شروع بشه نه 0/60 + final barProgress = (i + 1) / barCount; final paint = barProgress <= progress && isActive ? activePaint : inactivePaint; canvas.drawLine( diff --git a/lib/views/podcasts/studio_details/studio_details.mobile.dart b/lib/views/podcasts/studio_details/studio_details.mobile.dart index 94ac04d..1192075 100644 --- a/lib/views/podcasts/studio_details/studio_details.mobile.dart +++ b/lib/views/podcasts/studio_details/studio_details.mobile.dart @@ -1,21 +1,31 @@ // ignore_for_file: use_build_context_synchronously, deprecated_member_use 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/models/view/app_bar_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'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; import 'package:didvan/views/podcasts/studio_details/widgets/studio_details_widget.dart'; import 'package:didvan/views/widgets/bookmark_button.dart'; import 'package:didvan/views/widgets/audio/audio_player_widget.dart'; -import 'package:didvan/views/widgets/didvan/app_bar.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/overview/multitype.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; +import 'package:didvan/views/widgets/tag_item.dart'; import 'package:didvan/views/widgets/video/primary_controls.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; -import 'package:didvan/routes/routes.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:video_player/video_player.dart'; class StudioDetails extends StatefulWidget { @@ -27,14 +37,132 @@ class StudioDetails extends StatefulWidget { State createState() => _StudioDetailsState(); } -class _StudioDetailsState extends State { +class _StudioDetailsState extends State + with TickerProviderStateMixin, WidgetsBindingObserver { int _currentlyPlayingId = 0; VideoPlayerController? _videoPlayerController; ChewieController? _chewieController; + bool _isDescriptionExpanded = false; + final _focusNode = FocusNode(); + + late AnimationController _mainAnimationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + late AnimationController _playerAnimationController; + late Animation _playerScaleAnimation; + late Animation _playerFadeAnimation; + + late AnimationController _titleAnimationController; + late Animation _titleSlideAnimation; + late Animation _titleFadeAnimation; + + late AnimationController _tagsAnimationController; + late Animation _tagsFadeAnimation; + + late AnimationController _bookmarkAnimationController; + late Animation _bookmarkScaleAnimation; + late Animation _bookmarkRotationAnimation; + + final GlobalKey _relatedContentKey = + GlobalKey(); + final GlobalKey _commentsKey = + GlobalKey(); @override void initState() { super.initState(); + + _mainAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _mainAnimationController, + curve: const Interval(0.0, 0.5, curve: Curves.easeIn), + ), + ); + + _slideAnimation = + Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( + CurvedAnimation( + parent: _mainAnimationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOutCubic), + ), + ); + + _playerAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + ); + + _playerScaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: _playerAnimationController, + curve: Curves.elasticOut, + ), + ); + + _playerFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _playerAnimationController, + curve: const Interval(0.0, 0.5, curve: Curves.easeIn), + ), + ); + + _titleAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + _titleSlideAnimation = + Tween(begin: const Offset(-0.3, 0), end: Offset.zero).animate( + CurvedAnimation( + parent: _titleAnimationController, + curve: Curves.easeOutBack, + ), + ); + + _titleFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _titleAnimationController, + curve: Curves.easeIn, + ), + ); + + _tagsAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + + _tagsFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _tagsAnimationController, + curve: Curves.easeIn, + ), + ); + + _bookmarkAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + + _bookmarkScaleAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _bookmarkAnimationController, + curve: Curves.elasticOut, + ), + ); + + _bookmarkRotationAnimation = Tween(begin: -0.5, end: 0.0).animate( + CurvedAnimation( + parent: _bookmarkAnimationController, + curve: Curves.easeOut, + ), + ); + final state = context.read(); state.args = widget.pageData['args']; @@ -43,32 +171,62 @@ class _StudioDetailsState extends State { () => state.getStudioDetails(widget.pageData['id']).then((_) { if (mounted) { _initializePlayer(state.studio); + Future.delayed(const Duration(milliseconds: 300), () { + if (mounted) { + state.getRelatedContents(); + _mainAnimationController.forward(); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) { + _playerAnimationController.forward(); + } + }); + Future.delayed(const Duration(milliseconds: 400), () { + if (mounted) { + _titleAnimationController.forward(); + } + }); + Future.delayed(const Duration(milliseconds: 600), () { + if (mounted) { + _tagsAnimationController.forward(); + } + }); + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) { + _bookmarkAnimationController.forward(); + } + }); + } + }); } }), ); + } - if (widget.pageData['goToComment'] != null) { - var openComments = widget.pageData['goToComment']; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + WidgetsBinding.instance.addObserver(this); + } - if (openComments) { - Future.delayed( - const Duration(seconds: 1), - () => Navigator.of(context).pushNamed( - Routes.mentions, - arguments: { - 'id': context.read().studio.id, - 'type': 'studio', - 'title': context.read().studio.title, - }, - ), - ); - } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive) { + _stopPodcast(); } } + void _stopPodcast() { + if (MediaService.audioPlayer.playing) { + MediaService.audioPlayer.stop(); + } + MediaService.currentPodcast = null; + MediaService.audioPlayerTag = null; + } + Future _initializePlayer(StudioDetailsData studio) async { if (studio.type == 'video') { - // Disposing old controllers before creating new ones. _videoPlayerController?.dispose(); _chewieController?.dispose(); @@ -96,25 +254,28 @@ class _StudioDetailsState extends State { } catch (e) { debugPrint("Error initializing video player: $e"); } - } else { - // Handle audio playback using MediaService + } else if (studio.type == 'podcast') { await MediaService.handleAudioPlayback( audioSource: studio.link, id: studio.id, isVoiceMessage: false, ); - _currentlyPlayingId = studio.id; + if (mounted) { + setState(() { + _currentlyPlayingId = studio.id; + }); + } } } @override Widget build(BuildContext context) { - final d = MediaQuery.of(context); return Consumer( builder: (context, state, child) { if (state.isStudioLoaded && _currentlyPlayingId != state.studio.id) { Future.microtask(() => _initializePlayer(state.studio)); } + return StateHandler( state: state, onRetry: () { @@ -126,14 +287,17 @@ class _StudioDetailsState extends State { }, builder: (context, state) { if (!state.isStudioLoaded) { - return Center( - child: Image.asset( - Assets.loadingAnimation, - width: 100, - height: 100, + return Scaffold( + body: Center( + child: Image.asset( + Assets.loadingAnimation, + width: 100, + height: 100, + ), ), ); } + return WillPopScope( onWillPop: () async { if (MediaService.currentPodcast != null) { @@ -142,58 +306,116 @@ class _StudioDetailsState extends State { state.handleTracking(id: state.studio.id); return true; }, - child: SafeArea( - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(56), - child: DidvanAppBar( - appBarData: AppBarData( - trailing: BookmarkButton( - itemId: state.studio.id, - type: state.studio.type == 'video' - ? 'video' - : 'podcast', - value: state.studio.marked, - onMarkChanged: (value) { - widget.pageData['onMarkChanged']( - state.studio.id, value, true); - }, - gestureSize: 48, + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(90.0), + child: AppBar( + backgroundColor: Colors.white, + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Center( + child: SvgPicture.asset( + 'lib/assets/images/logos/logo-horizontal-light.svg', + height: 55, + ), + ), + IconButton( + icon: SvgPicture.asset( + 'lib/assets/icons/arrow-left.svg', + color: + const Color.fromARGB(255, 102, 102, 102), + height: 24, + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), ), - isSmall: true, - title: state.studio.title, ), - ), - ), - body: SingleChildScrollView( - child: SizedBox( - height: d.size.height - d.padding.top - 56, + )), + body: SingleChildScrollView( + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (state.studio.type == 'video') - AspectRatio( - aspectRatio: 16 / 9, - child: (_chewieController != null && - _chewieController!.videoPlayerController - .value.isInitialized) - ? Chewie(controller: _chewieController!) - : Center( - child: Image.asset( - Assets.loadingAnimation, - width: 100, - height: 100, + Hero( + tag: 'media-${state.studio.id}', + child: FadeTransition( + opacity: _playerFadeAnimation, + child: ScaleTransition( + scale: _playerScaleAnimation, + child: Stack( + children: [ + if (state.studio.type == 'video') + AspectRatio( + aspectRatio: 16 / 9, + child: (_chewieController != null && + _chewieController! + .videoPlayerController + .value + .isInitialized) + ? Chewie( + controller: _chewieController!) + : Center( + child: Image.asset( + Assets.loadingAnimation, + width: 100, + height: 100, + ), + ), + ), + if (state.studio.type == 'podcast') + AudioPlayerWidget(podcast: state.studio), + Positioned( + top: 1, + left: 1, + child: ScaleTransition( + scale: _bookmarkScaleAnimation, + child: RotationTransition( + turns: _bookmarkRotationAnimation, + child: BookmarkButton( + value: state.studio.marked, + onMarkChanged: (value) { + if (widget.pageData[ + 'onMarkChanged'] != + null) { + widget.pageData[ + 'onMarkChanged']( + state.studio.id, + value, + true); + } + }, + gestureSize: 35, + type: state.studio.type == 'video' + ? 'video' + : 'podcast', + itemId: state.studio.id, + ), + ), ), ), - ), - if (state.studio.type == 'podcast') - AudioPlayerWidget(podcast: state.studio), - Expanded( - child: StudioDetailsWidget( - onMarkChanged: (id, value) => widget - .pageData['onMarkChanged'](id, value, true), + ], + ), + ), ), ), + _buildDescriptionSection(state), + _buildRelatedContentSection(state), + _buildCommentsSection(state), ], ), ), @@ -207,10 +429,490 @@ class _StudioDetailsState extends State { ); } + Widget _buildDescriptionSection(StudioDetailsState state) { + 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: [ + SlideTransition( + position: _titleSlideAnimation, + child: FadeTransition( + opacity: _titleFadeAnimation, + child: 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)), + ), + ), + ), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: + Radius.circular(_isDescriptionExpanded ? 0 : 16.0), + ), + boxShadow: _isDescriptionExpanded + ? null + : [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8.0, + spreadRadius: -2.0, + offset: const Offset(0, 6), + ), + ], + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + AnimatedSize( + duration: const Duration(milliseconds: 300), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular( + _isDescriptionExpanded ? 0 : 16.0), + top: Radius.circular( + _isDescriptionExpanded ? 0 : 16.0), + ), + child: Container( + color: Theme.of(context).colorScheme.surface, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: _isDescriptionExpanded + ? double.infinity + : 140.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, + ), + }, + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _isDescriptionExpanded ? 0.0 : 1.0, + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(16.0), + top: Radius.circular(16.0), + ), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context) + .colorScheme + .surface + .withOpacity(0.5), + Theme.of(context) + .colorScheme + .surface + .withOpacity(0.9), + ], + stops: const [0.0, 0.5], + ), + ), + ), + ), + ), + ], + ), + ), + Transform.translate( + offset: const Offset(0, -14.0), + child: InkWell( + onTap: () { + setState(() { + _isDescriptionExpanded = !_isDescriptionExpanded; + }); + }, + child: Padding( + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(5.0), + decoration: const BoxDecoration( + color: Color.fromARGB(255, 230, 243, 250), + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + _isDescriptionExpanded + ? 'lib/assets/icons/arrow-up2.svg' + : 'lib/assets/icons/arrow-down.svg', + color: Theme.of(context).primaryColor, + height: 25, + ), + ), + ], + ), + ), + ), + ), + if (state.studio.tags.isNotEmpty) const SizedBox(height: 16), + if (state.studio.tags.isNotEmpty) + FadeTransition( + opacity: _tagsFadeAnimation, + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var i = 0; i < state.studio.tags.length; i++) + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 400 + (i * 100)), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Opacity( + opacity: value, + child: child, + ), + ); + }, + child: TagItem( + tag: state.studio.tags[i], + onMarkChanged: (id, value) { + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged']( + id, value, true); + } + }, + type: state.studio.type == 'video' + ? 'video' + : 'podcast', + ), + ), + ], + ), + ), + ], + ), + ), + 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 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}, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildRelatedContentSection(StudioDetailsState state) { + debugPrint("تعداد مطالب مرتبط: ${state.studio.relatedContents.length}"); + debugPrint( + "آیا لیست مطالب مرتبط خالی است؟ ${state.studio.relatedContentsIsEmpty}"); + debugPrint("تعداد tags: ${state.studio.tags.length}"); + + return Container( + width: double.infinity, + margin: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + "مطالب مرتبط:", + style: TextStyle( + fontSize: 18, + color: Color.fromARGB(255, 0, 53, 70), + fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Builder( + builder: (context) { + if (state.studio.relatedContents.isNotEmpty) { + 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: () { + _stopPodcast(); + + String routeName; + Map arguments; + + if (item.type == 'video') { + routeName = Routes.videoDetails; + arguments = { + 'id': item.id, + 'type': item.type, + 'onMarkChanged': (int id, bool value) { + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged']( + id, value, true); + } + }, + }; + } else if (item.type == 'podcast') { + routeName = Routes.studioDetails; + arguments = { + 'id': item.id, + 'type': item.type, + 'onMarkChanged': (int id, bool value, + [bool shouldUpdate = true]) { + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged']( + id, value, shouldUpdate); + } + }, + }; + } 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, + 'onMarkChanged': (int id, bool value, + [bool shouldUpdate = true]) { + // Match original + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged']( + id, value, shouldUpdate); + } + }, + }; + } + + Navigator.pushNamed( + context, + routeName, + arguments: arguments, + ); + }, + child: MultitypeOverview( + item: item, + onMarkChanged: (id, value) { + if (widget.pageData['onMarkChanged'] != + null) { + widget.pageData['onMarkChanged']( + id, value, true); // Modified + } + }, + ), + ), + ), + ), + ); + }, + ); + } else if (state.studio.relatedContentsIsEmpty) { + return const Padding( + padding: EdgeInsets.all(32.0), + child: Center( + child: DidvanText( + 'مطالب مرتبطی یافت نشد', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ); + } else { + return Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'در حال بارگذاری مطالب مرتبط...', + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ), + ...List.generate( + 3, + (index) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MultitypeOverview.placeholder, + )), + ], + ); + } + }, + ), + ), + ], + ), + ); + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); + + _stopPodcast(); + + _mainAnimationController.dispose(); + _playerAnimationController.dispose(); + _titleAnimationController.dispose(); + _tagsAnimationController.dispose(); + _bookmarkAnimationController.dispose(); + _videoPlayerController?.dispose(); _chewieController?.dispose(); + _focusNode.dispose(); super.dispose(); } } diff --git a/lib/views/widgets/audio/audio_player_widget.dart b/lib/views/widgets/audio/audio_player_widget.dart index 6b2d900..d017249 100644 --- a/lib/views/widgets/audio/audio_player_widget.dart +++ b/lib/views/widgets/audio/audio_player_widget.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'dart:math'; import 'package:didvan/config/design_config.dart'; @@ -10,16 +11,13 @@ import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; -import 'package:didvan/views/podcasts/podcasts_state.dart'; -import 'package:didvan/views/widgets/audio/audio_slider.dart'; -import 'package:didvan/views/widgets/bookmark_button.dart'; +import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; -import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/ink_wrapper.dart'; -import 'package:didvan/views/widgets/item_title.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:just_audio/just_audio.dart'; import 'package:provider/provider.dart'; @@ -30,217 +28,351 @@ class AudioPlayerWidget extends StatelessWidget { @override Widget build(BuildContext context) { final state = context.read(); - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), - color: Theme.of(context).colorScheme.surface, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 20), - height: 3, - width: 50, - color: Theme.of(context).colorScheme.hint, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: SkeletonImage( - imageUrl: podcast.image, - aspectRatio: 1 / 1, - ), - ), - const SizedBox(height: 16), - DidvanText( - podcast.title, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - StreamBuilder( - stream: MediaService.audioPlayer.speedStream, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox(); - } - return Column( - children: [ - PopupMenuButton( - child: Container( - width: 46, - alignment: Alignment.center, - margin: const EdgeInsets.fromLTRB(12, 0, 0, 46), - padding: const EdgeInsets.only(top: 2), - decoration: BoxDecoration( - borderRadius: DesignConfig.mediumBorderRadius, - border: Border.all( - color: - Theme.of(context).colorScheme.title)), - child: DidvanText( - '${snapshot.data!.toString().replaceAll('.0', '')}X'), - ), - onSelected: (value) async { - await MediaService.audioPlayer.setSpeed(value); - }, - itemBuilder: (BuildContext context) => - [ - popUpSpeed(value: 0.5), - popUpSpeed(value: 0.75), - popUpSpeed(value: 1.0), - popUpSpeed(value: 1.25), - popUpSpeed(value: 1.5), - popUpSpeed(value: 2.0), - ], - ), - ], - ); - }), - Expanded( - child: AudioSlider( - tag: 'podcast-${podcast.id}', - showTimer: true, - duration: podcast.duration, + return Stack( + children: [ + SkeletonImage( + imageUrl: podcast.image, + aspectRatio: 1 / 1.3, + borderRadius: BorderRadius.circular(0), + ), + + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24.0)), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 30, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + StreamBuilder( + stream: MediaService.audioPlayer.speedStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox(); + } + + return const Column( + children: [ + SizedBox(), + // PopupMenuButton( + // child: Container( + // width: 46, + // alignment: Alignment.center, + // margin: const EdgeInsets.fromLTRB(12, 0, 0, 0), // تغییر: 46 پایین حذف شد + // padding: const EdgeInsets.only(top: 2), + // decoration: BoxDecoration( + // borderRadius: DesignConfig.mediumBorderRadius, + // border: Border.all( + // color: + // Theme.of(context).colorScheme.title)), + // child: DidvanText( + // '${snapshot.data!.toString().replaceAll('.0', '')}X'), + // ), + // onSelected: (value) async { + // await MediaService.audioPlayer.setSpeed(value); + // }, + // itemBuilder: (BuildContext context) => + // [ + // popUpSpeed(value: 0.5), + // popUpSpeed(value: 0.75), + // popUpSpeed(value: 1.0), + // popUpSpeed(value: 1.25), + // popUpSpeed(value: 1.5), + // popUpSpeed(value: 2.0), + // ], + // ), + ], + ); + }), + Expanded( + child: StreamBuilder( + stream: MediaService.audioPlayer.positionStream, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + + return StreamBuilder( + stream: MediaService.audioPlayer.durationStream, + builder: (context, durationSnapshot) { + final totalDuration = durationSnapshot.data ?? + Duration(milliseconds: podcast.duration); + final progress = + totalDuration.inMilliseconds > 0 + ? position.inMilliseconds / + totalDuration.inMilliseconds + : 0.0; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + _formatDuration(totalDuration), + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .title + .withOpacity(0.7), + ), + ), + + const SizedBox(width: 8), + + Expanded( + child: AudioWaveformProgress( + progress: progress.clamp(0.0, 1.0), + isActive: true, + onChanged: (value) { + final newPosition = Duration( + milliseconds: + (totalDuration.inMilliseconds * + value) + .round(), + ); + MediaService.audioPlayer + .seek(newPosition); + }, + ), + ), + const SizedBox(width: 8), + + Text( + _formatDuration(position), + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .title + .withOpacity(0.7), + ), + ), + ], + ); + }, + ); + }, + ), + ), + ], ), ), + + const SizedBox(height: 16), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Center( + child: StatefulBuilder( + builder: (context, setState) => Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 20, 8), + child: Center( + child: IconButton( + icon: SvgPicture.asset( + 'lib/assets/icons/timer-pause.svg', + width: 35, + height: 35, + colorFilter: const ColorFilter.mode( + Color.fromARGB(255, 102, 102, 102), + BlendMode.srcIn, + ), + ), + onPressed: () => _showSleepTimer( + context, + state, + () => setState(() {}), + ), + ), + ), + ), + + // DidvanIconButton( + // icon: state.timer == null && + // !state.stopOnPodcastEnds + // ? DidvanIcons.sleep_timer_regular + // : DidvanIcons.sleep_enabled_regular, + // color: Theme.of(context).colorScheme.title, + // onPressed: () => _showSleepTimer( + // context, + // state, + // () => setState(() {}), + // ), + // ), + // if (state.timer != null) + // DidvanText( + // state.stopOnPodcastEnds + // ? 'پایان پادکست' + // : '\'${state.timerValue}', + // isEnglishFont: true, + // style: Theme.of(context).textTheme.labelSmall, + // color: Theme.of(context).colorScheme.title, + // ), + ], + ), + ), + ), + ), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: max( + 0, + MediaService + .audioPlayer.position.inSeconds + + 10, + ), + ), + ); + }, + icon: SvgPicture.asset( + 'lib/assets/icons/forward-10-seconds.svg'), + ), + ), + ), + ), + Expanded( + child: Center( + child: StreamBuilder( + stream: MediaService.audioPlayer.playerStateStream, + builder: (context, snapshot) { + if (snapshot.data == null) { + return const CircularProgressIndicator(); + } + return StreamBuilder( + stream: MediaService.audioPlayer.playingStream, + builder: (context, snapshot) { + final isPlaying = snapshot.data ?? false; + return _PlayPouseAnimatedIcon( + audioSource: podcast.link, + id: podcast.id, + isPlaying: isPlaying, + ); + }, + ); + }, + ), + ), + ), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: max( + 0, + MediaService + .audioPlayer.position.inSeconds - + 5, + ), + ), + ); + }, + icon: SvgPicture.asset( + 'lib/assets/icons/backward-5-seconds.svg'), + ), + ), + ), + ), + Expanded( + child: StreamBuilder( + stream: MediaService.audioPlayer.speedStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox(); + } + return Center( + child: PopupMenuButton( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Container( + width: 46, + alignment: Alignment.center, + margin: const EdgeInsets.fromLTRB( + 12, 0, 0, 0), // تغییر: 46 پایین حذف شد + padding: const EdgeInsets.only(top: 2), + decoration: BoxDecoration( + borderRadius: + DesignConfig.mediumBorderRadius, + border: Border.all( + color: const Color.fromARGB( + 255, 102, 102, 102))), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DidvanText( + '${snapshot.data!.toString().replaceAll('.0', '')}X', + style: const TextStyle( + fontWeight: FontWeight.w900, + color: Color.fromARGB( + 255, 102, 102, 102)), + ), + ), + ), + ), + onSelected: (value) async { + await MediaService.audioPlayer.setSpeed(value); + }, + itemBuilder: (BuildContext context) => + [ + popUpSpeed(value: 0.5), + popUpSpeed(value: 0.75), + popUpSpeed(value: 1.0), + popUpSpeed(value: 1.25), + popUpSpeed(value: 1.5), + popUpSpeed(value: 2.0), + ], + ), + ); + }, + ), + ), + ], + ), + // اضافه کردن کمی فاصله در پایین + const SizedBox(height: 16), ], ), ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Center( - child: StatefulBuilder( - builder: (context, setState) => Column( - children: [ - DidvanIconButton( - icon: state.timer == null && !state.stopOnPodcastEnds - ? DidvanIcons.sleep_timer_regular - : DidvanIcons.sleep_enabled_regular, - color: Theme.of(context).colorScheme.title, - onPressed: () => _showSleepTimer( - context, - state, - () => setState(() {}), - ), - ), - if (state.timer != null) - DidvanText( - state.stopOnPodcastEnds - ? 'پایان پادکست' - : '\'${state.timerValue}', - isEnglishFont: true, - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.title, - ), - ], - ), - ), - ), - ), - Expanded( - child: Center( - child: Column( - children: [ - DidvanIconButton( - color: Theme.of(context).colorScheme.title, - size: 32, - icon: DidvanIcons.media_forward_solid, - onPressed: () { - MediaService.audioPlayer.seek( - Duration( - seconds: - MediaService.audioPlayer.position.inSeconds + - 30, - ), - ); - }, - ), - DidvanText( - '30', - isEnglishFont: true, - color: Theme.of(context).colorScheme.title, - ), - ], - ), - ), - ), - Expanded( - child: Center( - child: StreamBuilder( - stream: MediaService.audioPlayer.playerStateStream, - builder: (context, snapshot) { - if (snapshot.data == null) { - return const CircularProgressIndicator(); - } - return StreamBuilder( - stream: MediaService.audioPlayer.playingStream, - builder: (context, snapshot) { - return _PlayPouseAnimatedIcon( - audioSource: podcast.link, - id: podcast.id, - ); - }, - ); - }, - ), - ), - ), - Expanded( - child: Center( - child: Column( - children: [ - DidvanIconButton( - size: 32, - icon: DidvanIcons.media_backward_solid, - color: Theme.of(context).colorScheme.title, - onPressed: () { - MediaService.audioPlayer.seek( - Duration( - seconds: max( - 0, - MediaService.audioPlayer.position.inSeconds - - 10, - ), - ), - ); - }, - ), - DidvanText( - '10', - isEnglishFont: true, - color: Theme.of(context).colorScheme.title, - ), - ], - ), - ), - ), - Expanded( - child: Center( - child: BookmarkButton( - itemId: state.studio.id, - type: 'podcast', - gestureSize: 48, - color: Theme.of(context).colorScheme.title, - value: podcast.marked, - onMarkChanged: (value) => context - .read() - .changeMark(podcast.id, value, true), - ), - ), - ), - ], - ), - ], - ), + ), + ], ); } + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + final seconds = duration.inSeconds.remainder(60); + + if (hours > 0) { + return '$hours:${twoDigits(minutes)}:${twoDigits(seconds)}'; + } else { + return '${twoDigits(minutes)}:${twoDigits(seconds)}'; + } + } + PopupMenuItem popUpSpeed({required double value}) { return PopupMenuItem( value: value, @@ -272,9 +404,22 @@ class AudioPlayerWidget extends StatelessWidget { content: StatefulBuilder( builder: (context, setState) => Column( children: [ - const ItemTitle( - title: 'زمان خواب', - icon: DidvanIcons.sleep_timer_regular, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'lib/assets/icons/timer-pause.svg', + height: 24, + color: const Color.fromARGB(255, 102, 102, 102), + ), + const SizedBox( + width: 10, + ), + const Text( + 'زمان خواب', + style: TextStyle(color: Color.fromARGB(255, 102, 102, 102)), + ) + ], ), const SizedBox(height: 24), DidvanText( @@ -378,41 +523,19 @@ class AudioPlayerWidget extends StatelessWidget { class _PlayPouseAnimatedIcon extends StatefulWidget { final String audioSource; final int id; + final bool isPlaying; const _PlayPouseAnimatedIcon( - {Key? key, required this.audioSource, required this.id}) + {Key? key, + required this.audioSource, + required this.id, + required this.isPlaying}) : super(key: key); @override State<_PlayPouseAnimatedIcon> createState() => __PlayPouseAnimatedIconState(); } -class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> - with SingleTickerProviderStateMixin { - late final AnimationController _animationController; - - @override - void didUpdateWidget(covariant _PlayPouseAnimatedIcon oldWidget) { - _handleAnimation(); - super.didUpdateWidget(oldWidget); - } - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: DesignConfig.lowAnimationDuration, - ); - } - - void _handleAnimation() { - if (MediaService.audioPlayer.playing) { - _animationController.forward(); - } else { - _animationController.reverse(); - } - } - +class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> { @override Widget build(BuildContext context) { return InkWrapper( @@ -423,27 +546,14 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> isVoiceMessage: false, id: widget.id, ); - _handleAnimation(); }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.title, - shape: BoxShape.circle, - ), - child: AnimatedIcon( - size: 40, - color: Theme.of(context).colorScheme.surface, - icon: AnimatedIcons.play_pause, - progress: _animationController, - ), + child: SvgPicture.asset( + widget.isPlaying + ? 'lib/assets/icons/pause-circle.svg' + : 'lib/assets/icons/video-circle.svg', + width: 65, + height: 65, ), ); } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } } diff --git a/lib/views/widgets/carousel_3d.dart b/lib/views/widgets/carousel_3d.dart index c69b820..f8ff2d3 100644 --- a/lib/views/widgets/carousel_3d.dart +++ b/lib/views/widgets/carousel_3d.dart @@ -189,11 +189,11 @@ class _Carousel3DState extends State with SingleTickerProviderStateM return AnimatedContainer( duration: const Duration(milliseconds: 150), margin: const EdgeInsets.symmetric(horizontal: 3), - width: _currentIndex == index ? 12.0 : 8.0, - height: _currentIndex == index ? 12.0 : 8.0, + width: _currentIndex == index ? 9.0 : 7.0, + height: _currentIndex == index ? 9.0 : 7.0, decoration: BoxDecoration( color: _currentIndex == index - ? const Color.fromRGBO(0, 69, 92, 1) + ? const Color.fromARGB(255, 0, 126, 167) : Colors.grey.withOpacity(0.5), shape: BoxShape.circle, ),