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