// ignore_for_file: deprecated_member_use import 'dart:async'; import 'dart:math'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/widgets/didvan/divider.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/overview/multitype.dart'; import 'package:didvan/views/widgets/skeleton_image.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:just_audio/just_audio.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:url_launcher/url_launcher_string.dart'; class DidvanPageView extends StatefulWidget { final List items; final int initialIndex; final int currentIndex; final bool isRadar; final void Function(int id, bool value) onMarkChanged; final ScrollController scrollController; final void Function(int index) onPageChanged; const DidvanPageView({ Key? key, required this.initialIndex, required this.items, required this.scrollController, required this.onPageChanged, required this.isRadar, required this.currentIndex, required this.onMarkChanged, }) : super(key: key); @override State createState() => _DidvanPageViewState(); } class _DidvanPageViewState extends State { static String? _lastPlayedUrl; String _formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, "0"); String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); return "${duration.inHours > 0 ? '${twoDigits(duration.inHours)}:' : ''}$twoDigitMinutes:$twoDigitSeconds"; } PopupMenuItem popUpSpeed({required double value}) { return PopupMenuItem( value: value, child: DidvanText('$value X'), ); } String _getCategoryIcon(String label) { if (label.contains('سیاسی')) { return 'lib/assets/icons/SiasiNoBG.svg'; } else if (label.contains('کسب')) { return 'lib/assets/icons/KasbokarNoBG.svg'; } else if (label.contains('اقتصادی')) { return 'lib/assets/icons/Economic_NoBG.svg'; } else if (label.contains('اجتماعی')) { return 'lib/assets/icons/EjtemaeiNoBg.svg'; } else if (label.contains('محیطی') || label.contains('زیست')) { return 'lib/assets/icons/ZistMohitNoBG.svg'; } else if (label.contains('فناوری')) { return 'lib/assets/icons/FanavariNoBG.svg'; } return 'lib/assets/icons/discovery.svg'; } @override void dispose() { MediaService.audioPlayer.stop(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, appBar: PreferredSize( preferredSize: const Size.fromHeight(90.0), child: AppBar( 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', 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: Column( children: [ Expanded( child: Stack( children: [ Directionality( textDirection: TextDirection.ltr, child: CarouselSlider.builder( itemCount: widget.items.length, options: CarouselOptions( onPageChanged: (index, reason) => widget.onPageChanged(index), height: double.infinity, initialPage: widget.initialIndex, viewportFraction: 1.0, enableInfiniteScroll: false, scrollPhysics: const BouncingScrollPhysics(), ), itemBuilder: (context, index, realIndex) { final item = widget.items[index]; if (item == null) { return const SizedBox(); } final bool isActive = index == widget.currentIndex; return Directionality( textDirection: TextDirection.rtl, child: LayoutBuilder( builder: (context, constraints) { final double imageHeight = constraints.maxWidth / (16 / 9); const double overlap = 24.0; return SizedBox( height: constraints.maxHeight, child: SingleChildScrollView( controller: isActive ? widget.scrollController : null, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.only(bottom: 92), child: Stack( children: [ Positioned( top: 0, left: 0, right: 0, height: imageHeight, child: SkeletonImage( imageUrl: item.image, borderRadius: BorderRadius.zero, width: double.infinity, ), ), if (widget.isRadar && item.categories != null && item.categories.isNotEmpty) Positioned( top: 16, left: 16, child: Wrap( spacing: 8.0, runSpacing: 8.0, direction: Axis.horizontal, alignment: WrapAlignment.start, children: [ for (var i = 0; i < item.categories.length; i++) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color.fromARGB( 255, 230, 242, 246), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( _getCategoryIcon(item .categories[i].label), height: 16, width: 16, color: const Color.fromARGB( 255, 25, 93, 128), ), const SizedBox(width: 6), DidvanText( item.categories[i].label, style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: const Color .fromARGB(255, 25, 93, 128), fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ), ], ), ), Container( margin: EdgeInsets.only( top: imageHeight - overlap), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .background, borderRadius: const BorderRadius.vertical( top: Radius.circular(24), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, -5), ), ], ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.isRadar && item.podcast != null) _AnimatedEntry( isActive: isActive, delay: 0, child: _buildPodcastStylePlayer( context, item), ), _AnimatedEntry( isActive: isActive, delay: 100, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: DidvanText( item.title, style: Theme.of(context) .textTheme .titleMedium, ), ), ), const SizedBox(height: 8), if (!widget.isRadar && item.description != null && item.description!.isNotEmpty) _AnimatedEntry( isActive: isActive, delay: 200, child: Padding( padding: const EdgeInsets .symmetric( horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment .start, children: [ DidvanText( 'خلاصه خبر:', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: Theme.of( context) .colorScheme .primary, fontWeight: FontWeight .bold, ), ), const SizedBox(height: 8), DidvanText( item.description!, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: const Color .fromARGB( 255, 102, 102, 102), )), const SizedBox(height: 2), const DidvanDivider(), ], ), ), ), if (widget.isRadar && item.description != null && item.description!.isNotEmpty) _AnimatedEntry( isActive: isActive, delay: 200, child: Padding( padding: const EdgeInsets .symmetric( horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment .start, children: [ DidvanText( 'خلاصه پویش افق:', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: Theme.of( context) .colorScheme .primary, fontWeight: FontWeight .bold, ), ), const SizedBox(height: 8), DidvanText( item.description!, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: const Color .fromARGB( 255, 102, 102, 102), ), ), const SizedBox(height: 2), const DidvanDivider(), ], ), ), ), _AnimatedEntry( isActive: isActive, delay: 300, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: _subtitle(item), ), ), _AnimatedEntry( isActive: isActive, delay: 400, child: Column( children: [ for (var i = 0; i < item.contents.length; i++) Padding( padding: const EdgeInsets .symmetric( horizontal: 16, vertical: 4, ), child: _contentBuilder( item, i), ), ], ), ), if (item.tags.isNotEmpty) const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: Wrap( spacing: 8, runSpacing: 8, children: [ for (var i = 0; i < item.tags.length; i++) TagItem( tag: item.tags[i], onMarkChanged: widget.onMarkChanged, type: widget.isRadar ? 'radar' : 'news', ), ], ), ), const Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, ), child: DidvanDivider( verticalPadding: 0), ), const Padding( padding: EdgeInsets.symmetric( horizontal: 16.0, vertical: 16.0), child: ItemTitle( title: 'مطالب مرتبط:'), ), if (item.relatedContents.isEmpty && !item.relatedContentsIsEmpty) for (var i = 0; i < 3; i++) Padding( padding: const EdgeInsets.only( bottom: 8, left: 16, right: 16, ), child: MultitypeOverview .placeholder, ), for (var i = 0; i < item.relatedContents.length; i++) Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 4), child: GestureDetector( onTap: () { final relatedItem = item.relatedContents[i]; if (relatedItem.type == 'news') { Navigator.of(context) .pushNamed( Routes.newsDetails, arguments: { 'id': relatedItem.id, 'args': const NewsRequestArgs( page: 0), 'onMarkChanged': (id, value) {}, 'hasUnmarkConfirmation': false, }, ); } else if (relatedItem .type == 'radar') { Navigator.of(context) .pushNamed( Routes.radarDetails, arguments: { 'id': relatedItem.id, 'args': const RadarRequestArgs( page: 0), 'onMarkChanged': (id, value) {}, 'onCommentsChanged': (id, count) {}, 'hasUnmarkConfirmation': false, }, ); } }, child: MultitypeOverview( item: item.relatedContents[i], onMarkChanged: (id, value) {}, ), ), ), const SizedBox(height: 8), ], ), ), ), ], ), ), ); }, ), ); }, ), ), ], ), ), ], ), ); } Widget _buildPodcastStylePlayer(BuildContext context, dynamic item) { return Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( height: 16, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Expanded( child: StreamBuilder( stream: MediaService.audioPlayer.positionStream, builder: (context, snapshot) { final position = snapshot.data ?? Duration.zero; return StreamBuilder( stream: MediaService.audioPlayer.durationStream, builder: (context, durationSnapshot) { final totalDuration = durationSnapshot.data ?? Duration( minutes: int.tryParse(item.duration.toString()) ?? 0); final progress = totalDuration.inMilliseconds > 0 ? position.inMilliseconds / totalDuration.inMilliseconds : 0.0; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ DidvanText( _formatDuration(totalDuration), style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.caption, ), ), 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), DidvanText( _formatDuration(position), style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.caption, ), ), ], ); }, ); }, ), ), ], ), ), const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( icon: SvgPicture.asset( 'lib/assets/icons/timer-pause.svg', width: 24, height: 24, color: Theme.of(context).colorScheme.caption, ), onPressed: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text("تایمر خواب در این بخش فعال نیست"))); }, ), IconButton( onPressed: () { MediaService.audioPlayer.seek( Duration( seconds: max( 0, MediaService.audioPlayer.position.inSeconds + 10), ), ); }, icon: SvgPicture.asset('lib/assets/icons/forward-10-seconds.svg'), ), StreamBuilder( stream: MediaService.audioPlayer.playerStateStream, builder: (context, snapshot) { final playerState = snapshot.data; final processingState = playerState?.processingState; final playing = playerState?.playing; if (processingState == ProcessingState.loading || processingState == ProcessingState.buffering) { return Container( margin: const EdgeInsets.all(8.0), width: 48.0, height: 48.0, child: const CircularProgressIndicator(), ); } else if (playing != true) { return IconButton( icon: const Icon(Icons.play_circle_fill, size: 68), color: Theme.of(context).primaryColor, onPressed: () async { if (_lastPlayedUrl != item.podcast) { _lastPlayedUrl = item.podcast; await MediaService.audioPlayer.setUrl(item.podcast); } MediaService.audioPlayer.play(); }, ); } else if (processingState != ProcessingState.completed) { return IconButton( icon: const Icon(Icons.pause_circle_filled, size: 68), color: Theme.of(context).primaryColor, onPressed: MediaService.audioPlayer.pause, ); } else { return IconButton( icon: const Icon(Icons.replay_circle_filled, size: 48), color: Theme.of(context).primaryColor, onPressed: () => MediaService.audioPlayer.seek(Duration.zero), ); } }, ), IconButton( onPressed: () { MediaService.audioPlayer.seek( Duration( seconds: max(0, MediaService.audioPlayer.position.inSeconds - 5), ), ); }, icon: SvgPicture.asset('lib/assets/icons/backward-5-seconds.svg'), ), StreamBuilder( stream: MediaService.audioPlayer.speedStream, builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(); return PopupMenuButton( child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius, border: Border.all( color: Theme.of(context).colorScheme.caption)), child: DidvanText( '${snapshot.data!.toString().replaceAll('.0', '')}X', style: TextStyle( fontWeight: FontWeight.w900, color: Theme.of(context).colorScheme.caption, fontSize: 12), ), ), onSelected: (value) async { await MediaService.audioPlayer.setSpeed(value); }, itemBuilder: (BuildContext context) => >[ popUpSpeed(value: 0.5), popUpSpeed(value: 0.75), popUpSpeed(value: 1.0), popUpSpeed(value: 1.25), popUpSpeed(value: 1.5), popUpSpeed(value: 2.0), ], ); }, ), ], ), const SizedBox(height: 16), ], ); } Widget _contentBuilder(dynamic item, int index) { final content = item.contents[index]; if (content.text != null) { return Html( data: content.text, onAnchorTap: (href, _, element) { if (href!.contains('navigate-')) { if (href.contains('statistic')) { Navigator.of(navigatorKey.currentContext!) .pushNamed(Routes.statisticDetails, arguments: { 'onMarkChanged': (value) {}, 'label': href.split('-')[2], 'title': href.split('-').last, 'marked': false, }); } else if (href.contains('radar')) { Navigator.of(navigatorKey.currentContext!).pushNamed( Routes.radarDetails, arguments: { 'onMarkChanged': (id, value) {}, 'onCommentsChanged': (id, count) {}, 'id': int.parse(href.split('-').last), 'args': const RadarRequestArgs(page: 0), 'hasUnmarkConfirmation': false, }, ); } else if (href.contains('news')) { Navigator.of(navigatorKey.currentContext!).pushNamed( Routes.newsDetails, arguments: { 'onMarkChanged': (id, value) {}, 'id': int.parse(href.split('-').last), 'args': const NewsRequestArgs(page: 0), 'hasUnmarkConfirmation': false, }, ); } } else if (href.contains('popup-')) { showDialog( context: context, builder: (context) => Dialog( shape: const RoundedRectangleBorder( borderRadius: DesignConfig.lowBorderRadius, ), alignment: Alignment.center, child: SizedBox( width: ActionSheetUtils(context).mediaQueryData.size.width, child: DidvanCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon( DidvanIcons.info_circle_solid, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), DidvanText( element!.text, style: Theme.of(context).textTheme.titleSmall, ), ], ), const SizedBox(height: 8), DidvanText(href.split('-').last), const SizedBox(height: 16), DidvanButton( title: 'بستن', onPressed: ActionSheetUtils(context).pop, ), ], ), ), ), ), ); } else { AppInitializer.openWebLink(context, href, mode: LaunchMode.inAppWebView); } }, style: { '*': Style( direction: TextDirection.rtl, textAlign: TextAlign.right, lineHeight: LineHeight.percent(135), margin: const Margins(), padding: HtmlPaddings.zero, color: const Color.fromRGBO(102, 102, 102, 1), ), 'a': Style( textAlign: TextAlign.left, color: const Color.fromARGB(255, 0, 126, 167), textDecoration: TextDecoration.none, fontWeight: FontWeight.normal, ), }, ); } if (content.image != null) { return Column( children: [ GestureDetector( onTap: () { showDialog( context: context, builder: (context) => Stack( children: [ Positioned.fill( child: InteractiveViewer( child: Center( child: SkeletonImage( width: min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height), imageUrl: content.largeImage ?? content.image, ), ), ), ), const Positioned( right: 24, top: 24, child: _BackButton(), ), ], ), ); }, child: SkeletonImage( imageUrl: content.image!, width: double.infinity, ), ), if (content.caption != null) Center( child: DidvanText( content.caption, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), ), ], ); } return const SizedBox(); } Widget _subtitle(dynamic item) { if (widget.isRadar) { return Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ SvgPicture.asset('lib/assets/icons/calendar.svg'), const SizedBox( width: 5, ), DidvanText( DateTime.parse(item.createdAt!).toPersianDateStr(), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.caption, ), ), ], ); } else { return Row( children: [ SvgPicture.asset('lib/assets/icons/calendar.svg'), const SizedBox( width: 5, ), DidvanText( DateTime.parse(item.createdAt!).toPersianDateStr(), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.caption, ), ), ], ); } } } class _BackButton extends StatefulWidget { final ScrollController? scrollController; // ignore: unused_element_parameter const _BackButton({Key? key, this.scrollController}) : super(key: key); @override __BackButtonState createState() => __BackButtonState(); } class __BackButtonState extends State<_BackButton> { bool _isVisible = false; @override void didUpdateWidget(covariant _BackButton oldWidget) { if (widget.scrollController != null) { _isVisible = false; _handleScroll(); } super.didUpdateWidget(oldWidget); } @override void initState() { if (widget.scrollController != null) { _handleScroll(); } else { _isVisible = true; } super.initState(); } void _handleScroll() { widget.scrollController!.addListener(() { final position = widget.scrollController!.position.pixels; final size = MediaQuery.of(context).size.height * 0.3; if (position > size && _isVisible == false) { setState(() { _isVisible = true; }); } if (position < size && _isVisible == true) { setState(() { _isVisible = false; }); } }); } @override Widget build(BuildContext context) { return AnimatedVisibility( duration: DesignConfig.lowAnimationDuration, isVisible: _isVisible, child: InkWrapper( borderRadius: DesignConfig.lowBorderRadius, onPressed: Navigator.of(context).pop, child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.splash, border: Border.all(color: Theme.of(context).colorScheme.border), borderRadius: DesignConfig.lowBorderRadius, ), child: const Icon( DidvanIcons.back_regular, size: 32, ), ), ), ); } } class _AnimatedEntry extends StatefulWidget { final Widget child; final int delay; final bool isActive; const _AnimatedEntry({ Key? key, required this.child, required this.delay, required this.isActive, }) : super(key: key); @override State<_AnimatedEntry> createState() => _AnimatedEntryState(); } class _AnimatedEntryState extends State<_AnimatedEntry> { bool _shouldAnimate = false; Timer? _timer; @override void initState() { super.initState(); _checkAnimation(); } @override void didUpdateWidget(covariant _AnimatedEntry oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive != oldWidget.isActive) { _checkAnimation(); } } @override void dispose() { _timer?.cancel(); super.dispose(); } void _checkAnimation() { if (widget.isActive) { _timer?.cancel(); _timer = Timer(Duration(milliseconds: widget.delay), () { if (mounted) { setState(() { _shouldAnimate = true; }); } }); } else { _timer?.cancel(); if (_shouldAnimate) { setState(() { _shouldAnimate = false; }); } } } @override Widget build(BuildContext context) { return AnimatedOpacity( duration: const Duration(milliseconds: 500), curve: Curves.easeOut, opacity: _shouldAnimate ? 1.0 : 0.0, child: AnimatedPadding( duration: const Duration(milliseconds: 600), curve: Curves.easeOutQuart, padding: EdgeInsets.only(top: _shouldAnimate ? 0 : 50), child: widget.child, ), ); } }