redesign single podcast page

This commit is contained in:
mohamadmahdi jebeli 2025-10-20 11:48:01 +03:30
parent 386ed90101
commit 84422637d9
10 changed files with 1401 additions and 581 deletions

View File

@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 16.5C5.3775 16.5 2.4375 13.56 2.4375 9.9375C2.4375 6.315 5.3775 3.375 9 3.375C12.6225 3.375 15.5625 6.315 15.5625 9.9375" stroke="#FCFCFC" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 6V9.75" stroke="#FCFCFC" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 1.5H11.25" stroke="#FCFCFC" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.25 12.75V15.75" stroke="#FCFCFC" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 12.75V15.75" stroke="#FCFCFC" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@ -0,0 +1,3 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 4.66699C15.12 4.66699 4.66667 15.1203 4.66667 28.0003C4.66667 40.8803 15.12 51.3337 28 51.3337C40.88 51.3337 51.3333 40.8803 51.3333 28.0003C51.3333 15.1203 40.88 4.66699 28 4.66699ZM34.2067 32.037L31.22 33.7637L28.2333 35.4903C24.3833 37.707 21.2333 35.887 21.2333 31.4537V28.0003V24.547C21.2333 20.0903 24.3833 18.2937 28.2333 20.5103L31.22 22.237L34.2067 23.9637C38.0567 26.1803 38.0567 29.8203 34.2067 32.037Z" fill="#007EA7"/>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@ -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';

View File

@ -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<Comment> {
late final CommentsState state;
bool _showSubComments = false;
CommentData get _comment => widget.comment;
@override
@ -51,191 +48,147 @@ class CommentState extends State<Comment> {
_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<void> _showCommentActions(comment) async {
ActionSheetUtils(context).showBottomSheet(
data: ActionSheetData(

View File

@ -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<MediaPage> {
});
},
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(

View File

@ -289,93 +289,127 @@ class _VideoDetailsPageState extends State<VideoDetailsPage>
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<VideoDetailsPage>
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: [

View File

@ -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(

View File

@ -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<StudioDetails> createState() => _StudioDetailsState();
}
class _StudioDetailsState extends State<StudioDetails> {
class _StudioDetailsState extends State<StudioDetails>
with TickerProviderStateMixin, WidgetsBindingObserver {
int _currentlyPlayingId = 0;
VideoPlayerController? _videoPlayerController;
ChewieController? _chewieController;
bool _isDescriptionExpanded = false;
final _focusNode = FocusNode();
late AnimationController _mainAnimationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late AnimationController _playerAnimationController;
late Animation<double> _playerScaleAnimation;
late Animation<double> _playerFadeAnimation;
late AnimationController _titleAnimationController;
late Animation<Offset> _titleSlideAnimation;
late Animation<double> _titleFadeAnimation;
late AnimationController _tagsAnimationController;
late Animation<double> _tagsFadeAnimation;
late AnimationController _bookmarkAnimationController;
late Animation<double> _bookmarkScaleAnimation;
late Animation<double> _bookmarkRotationAnimation;
final GlobalKey<AnimatedListState> _relatedContentKey =
GlobalKey<AnimatedListState>();
final GlobalKey<AnimatedListState> _commentsKey =
GlobalKey<AnimatedListState>();
@override
void initState() {
super.initState();
_mainAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _mainAnimationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_slideAnimation =
Tween<Offset>(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<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: _playerAnimationController,
curve: Curves.elasticOut,
),
);
_playerFadeAnimation = Tween<double>(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<Offset>(begin: const Offset(-0.3, 0), end: Offset.zero).animate(
CurvedAnimation(
parent: _titleAnimationController,
curve: Curves.easeOutBack,
),
);
_titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _titleAnimationController,
curve: Curves.easeIn,
),
);
_tagsAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_tagsFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _tagsAnimationController,
curve: Curves.easeIn,
),
);
_bookmarkAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_bookmarkScaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _bookmarkAnimationController,
curve: Curves.elasticOut,
),
);
_bookmarkRotationAnimation = Tween<double>(begin: -0.5, end: 0.0).animate(
CurvedAnimation(
parent: _bookmarkAnimationController,
curve: Curves.easeOut,
),
);
final state = context.read<StudioDetailsState>();
state.args = widget.pageData['args'];
@ -43,32 +171,62 @@ class _StudioDetailsState extends State<StudioDetails> {
() => 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<StudioDetailsState>().studio.id,
'type': 'studio',
'title': context.read<StudioDetailsState>().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<void> _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<StudioDetails> {
} 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<StudioDetailsState>(
builder: (context, state, child) {
if (state.isStudioLoaded && _currentlyPlayingId != state.studio.id) {
Future.microtask(() => _initializePlayer(state.studio));
}
return StateHandler<StudioDetailsState>(
state: state,
onRetry: () {
@ -126,14 +287,17 @@ class _StudioDetailsState extends State<StudioDetails> {
},
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<StudioDetails> {
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<StudioDetails> {
);
}
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<double>(
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<CommentsState>(
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<UserProvider>(
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<Offset>(
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<String, dynamic> 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();
}
}

View File

@ -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<StudioDetailsState>();
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<double>(
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) =>
<PopupMenuEntry>[
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<double>(
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) =>
// <PopupMenuEntry>[
// 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<Duration>(
stream: MediaService.audioPlayer.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return StreamBuilder<Duration?>(
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<PlayerState>(
stream: MediaService.audioPlayer.playerStateStream,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const CircularProgressIndicator();
}
return StreamBuilder<bool>(
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<double>(
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) =>
<PopupMenuEntry>[
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<PlayerState>(
stream: MediaService.audioPlayer.playerStateStream,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const CircularProgressIndicator();
}
return StreamBuilder<bool>(
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<PodcastsState>()
.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<dynamic> 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();
}
}

View File

@ -189,11 +189,11 @@ class _Carousel3DState extends State<Carousel3D> 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,
),