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,
),