// ignore_for_file: deprecated_member_use, undefined_prefixed_name import 'dart:ui_web' as ui_web; import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/enums.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/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:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher_string.dart'; class StudioDetails extends StatefulWidget { final Map pageData; const StudioDetails({super.key, required this.pageData}); @override State createState() => _StudioDetailsState(); } class _StudioDetailsState extends State with TickerProviderStateMixin { bool _isDescriptionExpanded = false; final _focusNode = FocusNode(); late AnimationController _mainAnimationController; late Animation _fadeAnimation; late Animation _slideAnimation; late AnimationController _playerAnimationController; late Animation _playerScaleAnimation; late Animation _playerFadeAnimation; late AnimationController _titleAnimationController; late Animation _titleSlideAnimation; late Animation _titleFadeAnimation; late AnimationController _tagsAnimationController; late Animation _tagsFadeAnimation; late AnimationController _bookmarkAnimationController; late Animation _bookmarkScaleAnimation; late Animation _bookmarkRotationAnimation; @override void initState() { super.initState(); final state = context.read(); state.args = widget.pageData['args']; _mainAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _mainAnimationController, curve: const Interval(0.0, 0.5, curve: Curves.easeIn), ), ); _slideAnimation = Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( CurvedAnimation( parent: _mainAnimationController, curve: const Interval(0.0, 0.6, curve: Curves.easeOutCubic), ), ); _playerAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), ); _playerScaleAnimation = Tween(begin: 0.8, end: 1.0).animate( CurvedAnimation( parent: _playerAnimationController, curve: Curves.elasticOut, ), ); _playerFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _playerAnimationController, curve: const Interval(0.0, 0.5, curve: Curves.easeIn), ), ); _titleAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _titleSlideAnimation = Tween(begin: const Offset(-0.3, 0), end: Offset.zero).animate( CurvedAnimation( parent: _titleAnimationController, curve: Curves.easeOutBack, ), ); _titleFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _titleAnimationController, curve: Curves.easeIn, ), ); _tagsAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _tagsFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _tagsAnimationController, curve: Curves.easeIn, ), ); _bookmarkAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _bookmarkScaleAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _bookmarkAnimationController, curve: Curves.elasticOut, ), ); _bookmarkRotationAnimation = Tween(begin: -0.5, end: 0.0).animate( CurvedAnimation( parent: _bookmarkAnimationController, curve: Curves.easeOut, ), ); Future.delayed( Duration.zero, () => state.getStudioDetails(widget.pageData['id']).then((_) { if (mounted) { 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(); }); } }); } }), ); } @override void dispose() { _mainAnimationController.dispose(); _playerAnimationController.dispose(); _titleAnimationController.dispose(); _tagsAnimationController.dispose(); _bookmarkAnimationController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Consumer( builder: (context, state, child) => StateHandler( state: state, onRetry: () => state.getStudioDetails(state.studio.id), builder: (context, state) { if (!state.isStudioLoaded) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: Center( child: Image.asset( Assets.loadingAnimation, width: 100, height: 100, ), ), ); } final String viewType = 'video-iframe-${state.studio.id}'; if (state.studio.type == 'video') { ui_web.platformViewRegistry.registerViewFactory( viewType, (int viewId) => html.IFrameElement() ..allowFullscreen = true ..src = Uri.dataFromString( '${state.studio.iframe!}', mimeType: 'text/html', ).toString() ..style.border = 'none', ); } return WillPopScope( onWillPop: () async { if (MediaService.currentPodcast != null) { state.studio = MediaService.currentPodcast!; } state.handleTracking(id: state.studio.id); return true; }, child: Scaffold( resizeToAvoidBottomInset: true, backgroundColor: Theme.of(context).colorScheme.surface, appBar: PreferredSize( preferredSize: const Size.fromHeight(90.0), child: AppBar( scrolledUnderElevation: 0, surfaceTintColor: Colors.transparent, backgroundColor: Theme.of(context).colorScheme.surface, 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', color: DesignConfig.isDark ? Theme.of(context).colorScheme.caption : null, height: 55, ), ), IconButton( icon: SvgPicture.asset( 'lib/assets/icons/arrow-left.svg', color: Theme.of(context).colorScheme.caption, height: 24, ), onPressed: () { Navigator.pop(context); }, ), ], ), ), ), )), body: SingleChildScrollView( physics: const ClampingScrollPhysics(), child: FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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: HtmlElementView( viewType: viewType, key: ValueKey(state.studio.id), ), ), if (state.studio.type == 'podcast') AudioPlayerWidget(podcast: state.studio), Positioned( top: 1, left: 1, child: ScaleTransition( scale: _bookmarkScaleAnimation, child: RotationTransition( turns: _bookmarkRotationAnimation, child: Row( children: [ Container( height: 36, decoration: BoxDecoration( color: Colors.black .withOpacity(0.6), shape: BoxShape.circle, ), child: IconButton( icon: SvgPicture.asset( 'lib/assets/icons/fluent_mention-32-regular.svg', width: 28, height: 28, ), onPressed: () => Navigator.of(context) .pushNamed( Routes.mentions, arguments: { 'id': state.studio.id, 'type': 'studio', 'title': state.studio.title, }, ), ), ), 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, ), ], ), ), ), ), ], ), ), ), ), _buildDescriptionSection(state), _buildRelatedContentSection(state), _buildCommentsSection(state), ], ), ), ), ), ), ); }, ), ); } Widget _buildDescriptionSection(StudioDetailsState state) { return AnimatedSwitcher( duration: const Duration(milliseconds: 500), child: KeyedSubtree( key: ValueKey('description-${state.studio.id}'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SlideTransition( position: _titleSlideAnimation, child: FadeTransition( opacity: _titleFadeAnimation, child: Padding( padding: const EdgeInsets.all(8.0), child: Text( state.studio.title, style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: DesignConfig.isDark ? const Color.fromARGB(255, 0, 90, 119) : const 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: BoxDecoration( color: Theme.of(context).colorScheme.focused, shape: BoxShape.circle, ), child: SvgPicture.asset( _isDescriptionExpanded ? 'lib/assets/icons/arrow-up2.svg' : 'lib/assets/icons/arrow-down.svg', color: Theme.of(context).primaryColor, height: 25, ), ), ], ), ), ), ), if (state.studio.tags.isNotEmpty) const SizedBox(height: 16), if (state.studio.tags.isNotEmpty) FadeTransition( opacity: _tagsFadeAnimation, child: Wrap( spacing: 8, runSpacing: 8, children: [ for (var i = 0; i < state.studio.tags.length; i++) TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 400 + (i * 100)), curve: Curves.easeOutBack, builder: (context, value, child) { return Transform.scale( scale: value, child: Opacity( opacity: value, child: child, ), ); }, child: TagItem( tag: state.studio.tags[i], onMarkChanged: (id, value) { if (widget.pageData['onMarkChanged'] != null) { widget.pageData['onMarkChanged']( id, value, true); } }, type: state.studio.type == 'video' ? 'video' : 'podcast', ), ), ], ), ), ], ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(), if (state.nextStudio != null && state.alongSideState == AppState.idle) StudioPreview( isNext: true, studio: state.nextStudio!, ), if (state.alongSideState == AppState.busy) StudioPreview.placeHolder, if (state.prevStudio != null && state.alongSideState == AppState.idle) StudioPreview( isNext: false, studio: state.prevStudio!, ), if (state.alongSideState == AppState.busy) StudioPreview.placeHolder, const SizedBox(), ], ), ], ), ), ); } Widget _buildCommentsSection(StudioDetailsState state) { return ChangeNotifierProvider( create: (context) => CommentsState() ..itemId = state.studio.id ..type = 'studio' ..onCommentsChanged = state.onCommentsChanged ..getComments(), child: Padding( padding: const EdgeInsets.all(8.0), child: Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Consumer( builder: (context, userProvider, child) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: CommentMessageBox(focusNode: _focusNode), ), ], ); }, ), SizedBox(height: state.studio.comments == 0 ? 0 : 16), SizedBox( width: double.infinity, child: state.studio.comments == 0 ? const SizedBox( height: 0, ) : DidvanText( 'نظرات کاربران:', style: TextStyle( color: DesignConfig.isDark ? const Color.fromARGB(255, 0, 90, 119) : const Color.fromARGB(255, 0, 53, 70), fontSize: 18, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 16), state.studio.comments == 0 ? const Padding( padding: EdgeInsets.all(32.0), child: SizedBox( height: 5, )) : SizedBox( height: 500, child: Comments( key: ValueKey('comments-${state.studio.id}'), pageData: const {'isPage': false}, ), ), ], ), ), ), ); } Widget _buildRelatedContentSection(StudioDetailsState state) { return Container( width: double.infinity, margin: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(8.0), child: Text( "مطالب مرتبط:", style: TextStyle( fontSize: 18, color: DesignConfig.isDark ? const Color.fromARGB(255, 0, 90, 119) : const 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 ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: state.studio.relatedContents.length, itemBuilder: (context, index) { final item = state.studio.relatedContents[index]; return 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, '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]) { 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); } }, ), ), ); }, ); } 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, )), ], ); } }, ), ), ], ), ); } }