diff --git a/lib/assets/icons/arrow-down.svg b/lib/assets/icons/arrow-down.svg new file mode 100644 index 0000000..c1464f2 --- /dev/null +++ b/lib/assets/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/arrow-left.svg b/lib/assets/icons/arrow-left.svg new file mode 100644 index 0000000..7098359 --- /dev/null +++ b/lib/assets/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/arrow-up2.svg b/lib/assets/icons/arrow-up2.svg new file mode 100644 index 0000000..b0e1075 --- /dev/null +++ b/lib/assets/icons/arrow-up2.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/bookmark_off.svg b/lib/assets/icons/bookmark_off.svg new file mode 100644 index 0000000..1d862cb --- /dev/null +++ b/lib/assets/icons/bookmark_off.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/bookmark_on.svg b/lib/assets/icons/bookmark_on.svg new file mode 100644 index 0000000..41cb3cf --- /dev/null +++ b/lib/assets/icons/bookmark_on.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 881f4f6..6c79f0e 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -64,6 +64,7 @@ import 'package:didvan/views/home/statistic/statistic_state.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart' if (dart.library.io) 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart' if (dart.library.html) 'package:didvan/views/podcasts/studio_details/studio_details.web.dart'; +import 'package:didvan/views/home/media/video_details_page.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/story_viewer/story_viewer_page.dart'; @@ -313,6 +314,20 @@ class RouteGenerator { return _errorRoute( 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.videoDetails: + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => StudioDetailsState(), + child: VideoDetailsPage( + pageData: settings.arguments as Map, + ), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.statisticDetails: if (settings.arguments is Map) { return _createRoute( diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 09376e3..05f8962 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -42,4 +42,5 @@ class Routes { static const String web = '/web'; static const String storyViewer = '/story-viewer'; static const String media = '/media'; + static const String videoDetails = '/video-details'; } \ No newline at end of file diff --git a/lib/views/home/media/video_details_page.dart b/lib/views/home/media/video_details_page.dart new file mode 100644 index 0000000..b7d7be9 --- /dev/null +++ b/lib/views/home/media/video_details_page.dart @@ -0,0 +1,507 @@ +// 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/assets.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/studio_details_data.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/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:url_launcher/url_launcher_string.dart'; +import 'package:video_player/video_player.dart'; + +class VideoDetailsPage extends StatefulWidget { + final Map pageData; + + const VideoDetailsPage({Key? key, required this.pageData}) : super(key: key); + + @override + State createState() => _VideoDetailsPageState(); +} + +class _VideoDetailsPageState extends State { + int _currentlyPlayingId = 0; + VideoPlayerController? _videoPlayerController; + ChewieController? _chewieController; + bool _isDescriptionExpanded = false; + + @override + void initState() { + super.initState(); + final state = context.read(); + state.args = widget.pageData['args']; + + Future.delayed( + Duration.zero, + () => state.getStudioDetails(widget.pageData['id']).then((_) { + if (mounted) { + _initializePlayer(state.studio); + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + state.getRelatedContents(); + } + }); + } + }), + ); + } + + Future _initializePlayer(StudioDetailsData studio) async { + if (studio.type == 'video') { + _videoPlayerController?.dispose(); + _chewieController?.dispose(); + + debugPrint("Playing video from URL: ${studio.link}"); + _videoPlayerController = VideoPlayerController.network(studio.link); + + try { + await _videoPlayerController!.initialize(); + if (mounted) { + setState(() { + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController!, + customControls: const PrimaryControls(), + autoPlay: true, + looping: true, + aspectRatio: 16 / 9, + materialProgressColors: ChewieProgressColors( + playedColor: Theme.of(context).colorScheme.title, + handleColor: Theme.of(context).colorScheme.title, + ), + ); + _currentlyPlayingId = studio.id; + }); + } + } catch (e) { + debugPrint("Error initializing video player: $e"); + } + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, child) { + if (state.isStudioLoaded && _currentlyPlayingId != state.studio.id) { + Future.microtask(() => _initializePlayer(state.studio)); + } + + return StateHandler( + state: state, + onRetry: () { + try { + state.getStudioDetails(state.studio.id); + } catch (e) { + state.getStudioDetails(widget.pageData['id']); + } + }, + builder: (context, state) { + if (!state.isStudioLoaded) { + return Scaffold( + body: Center( + child: Image.asset( + Assets.loadingAnimation, + width: 100, + height: 100, + ), + ), + ); + } + + return WillPopScope( + onWillPop: () async { + if (MediaService.currentPodcast != null) { + state.studio = MediaService.currentPodcast!; + } + state.handleTracking(id: state.studio.id); + return true; + }, + 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); + }, + ), + ], + ), + ), + ), + )), + 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, + ), + ), + 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), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } + + 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, + ), + 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, + ), + const SizedBox(width: 4), + Text( + _isDescriptionExpanded ? 'کمتر' : 'بیشتر', + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ), + 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', + ), + ], + ), + ], + ), + ), + 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, + }, + ), + ), + ), + ], + ), + ); + } + + 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: 16, + color: Color.fromARGB(255, 0, 53, 70), + fontWeight: FontWeight.bold), + ), + ), + Padding( + 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(), + ], + ); + } else if (state.studio.relatedContentsIsEmpty) { + debugPrint(" - Showing empty message"); + return const Padding( + padding: EdgeInsets.all(32.0), + child: Center( + child: DidvanText( + 'مطالب مرتبطی یافت نشد', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ); + } else { + debugPrint(" - Showing placeholders (loading state)"); + 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() { + _videoPlayerController?.dispose(); + _chewieController?.dispose(); + super.dispose(); + } +} diff --git a/lib/views/home/media/videocast_tab_page.dart b/lib/views/home/media/videocast_tab_page.dart index cdbb3c5..99bbc17 100644 --- a/lib/views/home/media/videocast_tab_page.dart +++ b/lib/views/home/media/videocast_tab_page.dart @@ -136,7 +136,7 @@ class _VideoCastTabPageState extends State { onTap: () { Navigator.pushNamed( context, - Routes.studioDetails, + Routes.videoDetails, arguments: { 'id': state.studios[_currentFeaturedIndex].id, 'type': state.studios[_currentFeaturedIndex].type, @@ -198,7 +198,7 @@ class _VideoCastTabPageState extends State { onTap: () { Navigator.pushNamed( context, - Routes.studioDetails, + Routes.videoDetails, arguments: { 'id': videocast.id, 'type': videocast.type, diff --git a/lib/views/home/media/widgets/featured_video_card.dart b/lib/views/home/media/widgets/featured_video_card.dart index cdd5671..7d65a09 100644 --- a/lib/views/home/media/widgets/featured_video_card.dart +++ b/lib/views/home/media/widgets/featured_video_card.dart @@ -25,7 +25,7 @@ class FeaturedVideoCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: MediaQuery.of(context).size.height * 0.40, + height: 380, margin: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), @@ -112,6 +112,7 @@ class FeaturedVideoCard extends StatelessWidget { style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: const Color.fromARGB(255, 200, 224, 244), fontWeight: FontWeight.bold, + fontSize: 18 ), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart b/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart index 70bc75c..60def50 100644 --- a/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart +++ b/lib/views/podcasts/studio_details/widgets/studio_details_widget.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/studio_details_data.dart'; import 'package:didvan/views/comments/comments.dart'; @@ -16,6 +14,8 @@ import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -37,7 +37,7 @@ class StudioDetailsWidget extends StatelessWidget { bool isVideo = state.studio.iframe != null; double topOffset = isVideo ? ds.width * 9 / 16 - : 400; // برای ویدیو aspect ratio، برای پادکست ارتفاع AudioPlayerWidget + : 400; return Container( height: max( ds.height - topOffset - 72 - MediaQuery.of(context).padding.top, @@ -101,20 +101,20 @@ class StudioDetailsWidget extends StatelessWidget { const SizedBox(), if (state.nextStudio != null && state.alongSideState == AppState.idle) - _StudioPreview( + StudioPreview( isNext: true, studio: state.nextStudio!, ), if (state.alongSideState == AppState.busy) - _StudioPreview.placeHolder, + StudioPreview.placeHolder, if (state.prevStudio != null && state.alongSideState == AppState.idle) - _StudioPreview( + StudioPreview( isNext: false, studio: state.prevStudio!, ), if (state.alongSideState == AppState.busy) - _StudioPreview.placeHolder, + StudioPreview.placeHolder, const SizedBox(), ], ), @@ -200,10 +200,10 @@ class StudioDetailsWidget extends StatelessWidget { } } -class _StudioPreview extends StatelessWidget { +class StudioPreview extends StatelessWidget { final bool isNext; final StudioDetailsData studio; - const _StudioPreview({ + const StudioPreview({ Key? key, required this.isNext, required this.studio, @@ -211,11 +211,18 @@ class _StudioPreview extends StatelessWidget { String get _previewTitle { if (studio.type == 'video') { - return 'ویدیوی ${isNext ? 'بعدی' : 'قبلی'} '; + return 'ویدیوکست ${isNext ? 'بعدی' : 'قبلی'} '; } return 'پادکست ${isNext ? 'بعدی' : 'قبلی'} '; } + 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')}'; + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -227,38 +234,94 @@ class _StudioPreview extends StatelessWidget { isForward: isNext, ); }, - child: Container( - width: 88, - height: 216, - color: Colors.transparent, - child: Column( - children: [ - SkeletonImage( - imageUrl: studio.image, - aspectRatio: 1 / 1, - ), - const SizedBox(height: 8), - Icon( - isNext - ? DidvanIcons.angle_right_regular - : DidvanIcons.angle_left_regular, - ), - const SizedBox(height: 8), - DidvanText( - _previewTitle, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - DidvanText( - studio.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.caption, - ), - ], + + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: 170, + height: 235, + decoration: BoxDecoration( + color: const Color.fromRGBO(235, 235, 235, 1), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: AspectRatio( + aspectRatio: 17 / 14, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SkeletonImage( + imageUrl: studio.image, + width: double.infinity, + height: double.infinity, + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 4, 8, 3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + DidvanText( + _previewTitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.normal, + color: const Color.fromARGB(255, 102, 102, 102), + fontSize: 12 + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + DidvanText( + studio.title, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.normal, + color: Colors.black87, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + SvgPicture.asset( + 'lib/assets/icons/clock.svg', + color: const Color.fromARGB(255, 102, 102, 102), + ), + const SizedBox(width: 4), + DidvanText( + _formatDuration(studio.duration).toPersianDigit(), + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: const Color.fromARGB(255, 102, 102, 102), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/views/widgets/bookmark_button.dart b/lib/views/widgets/bookmark_button.dart index 8067d3d..0578174 100644 --- a/lib/views/widgets/bookmark_button.dart +++ b/lib/views/widgets/bookmark_button.dart @@ -3,9 +3,9 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/utils/action_sheet.dart'; -import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class BookmarkButton extends StatefulWidget { final bool value; @@ -15,6 +15,9 @@ class BookmarkButton extends StatefulWidget { final double gestureSize; final String type; final int itemId; + final String? svgIconOn; + final String? svgIconOff; + const BookmarkButton({ Key? key, required this.value, @@ -24,6 +27,8 @@ class BookmarkButton extends StatefulWidget { required this.itemId, this.askForConfirmation = false, this.color, + this.svgIconOn, + this.svgIconOff, }) : super(key: key); @override @@ -45,38 +50,49 @@ class _BookmarkButtonState extends State { super.initState(); } + void _handleTap() async { + bool confirm = false; + if (widget.askForConfirmation) { + await ActionSheetUtils(context).openDialog( + data: ActionSheetData( + content: const DidvanText( + 'آیا می‌خواهید این محتوا از نشان‌ شده‌ها حذف شود؟', + ), + titleIcon: DidvanIcons.bookmark_regular, + titleColor: Theme.of(context).colorScheme.secondary, + title: 'تایید عملیات', + onConfirmed: () => confirm = true, + ), + ); + } + if (!widget.askForConfirmation || confirm) { + setState(() { + _value = !_value; + }); + widget.onMarkChanged(_value); + UserProvider.changeItemMark(widget.type, widget.itemId, _value); + } + } + @override Widget build(BuildContext context) { - return DidvanIconButton( - gestureSize: widget.gestureSize, - color: widget.color ?? - (DesignConfig.isDark || !_value - ? null - : Theme.of(context).colorScheme.primary), - icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular, - onPressed: () async { - bool confirm = false; - if (widget.askForConfirmation) { - await ActionSheetUtils(context).openDialog( - data: ActionSheetData( - content: const DidvanText( - 'آیا می‌خواهید این محتوا از نشان‌ شده‌ها حذف شود؟', - ), - titleIcon: DidvanIcons.bookmark_regular, - titleColor: Theme.of(context).colorScheme.secondary, - title: 'تایید عملیات', - onConfirmed: () => confirm = true, - ), - ); - } - if (!widget.askForConfirmation || confirm) { - setState(() { - _value = !_value; - }); - widget.onMarkChanged(_value); - UserProvider.changeItemMark(widget.type, widget.itemId, _value); - } - }, + print("BookmarkButton build - value: $_value"); + + return IconButton( + iconSize: widget.gestureSize, + onPressed: _handleTap, + icon: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: SvgPicture.asset( + _value ? 'lib/assets/icons/bookmark_on.svg' : 'lib/assets/icons/bookmark_off.svg', + key: ValueKey('bookmark_$_value'), + width: widget.gestureSize, + height: widget.gestureSize, + ), + ), ); } -} +} \ No newline at end of file diff --git a/lib/views/widgets/didvan/icon_button.dart b/lib/views/widgets/didvan/icon_button.dart index 3046757..774f44a 100644 --- a/lib/views/widgets/didvan/icon_button.dart +++ b/lib/views/widgets/didvan/icon_button.dart @@ -35,6 +35,7 @@ class DidvanIconButton extends StatelessWidget { size: size, color: color, ), + ), ); } diff --git a/lib/views/widgets/overview/multitype.dart b/lib/views/widgets/overview/multitype.dart index f156ffb..54ae5f0 100644 --- a/lib/views/widgets/overview/multitype.dart +++ b/lib/views/widgets/overview/multitype.dart @@ -6,10 +6,7 @@ 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/services/media/media.dart'; -import 'package:didvan/services/network/request.dart'; import 'package:didvan/utils/date_time.dart'; -import 'package:didvan/views/podcasts/studio_details/studio_details_state.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'; @@ -17,10 +14,8 @@ import 'package:didvan/views/widgets/didvan/text_field.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.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'; -import 'package:provider/provider.dart'; -import 'package:didvan/services/app_initalizer.dart'; -import 'package:url_launcher/url_launcher_string.dart'; class MultitypeOverview extends StatelessWidget { final OverviewData item; @@ -64,227 +59,190 @@ class MultitypeOverview extends StatelessWidget { return null; } - IconData get _icon { + String get _icon { switch (item.type) { case 'radar': - return DidvanIcons.scanning_light; + return 'DidvanIcons.scanning_light'; case 'news': - return DidvanIcons.foolad_light; + return 'DidvanIcons.foolad_light'; case 'video': - return DidvanIcons.video_light; + return 'DidvanIcons.video_light'; case 'podcast': - return DidvanIcons.podcast_light; + return 'DidvanIcons.podcast_light'; case 'delphi': case 'survey': - return DidvanIcons.saha_light; + return 'DidvanIcons.saha_light'; case 'infography': - return DidvanIcons.infography_regular; + return 'DidvanIcons.infography_regular'; default: - return DidvanIcons.radar_light; + return 'DidvanIcons.radar_light'; } } @override Widget build(BuildContext context) { - return DidvanCard( - onTap: () async { - if (item.type == 'infography') { - return; - } - if (item.type == 'podcast') { - final state = context.read(); - await state.getStudioDetails( - item.id, - args: const StudioRequestArgs(page: 0, type: 'podcast'), - ); - MediaService.currentPodcast = state.studio; - MediaService.handleAudioPlayback( - audioSource: item.link, - id: item.id, - isNetworkAudio: true, - isVoiceMessage: false, - ); - return; - } - if (_targetPageRouteName == null && item.link != null) { - AppInitializer.openWebLink( - context, - '${item.link!}?accessToken=${RequestService.token}', - mode: LaunchMode.inAppWebView, - ); - return; - } - Navigator.of(context).pushNamed( - _targetPageRouteName!, - arguments: { - 'onMarkChanged': onMarkChanged, - 'id': item.id, - 'args': _targetPageArgs, - 'hasUnmarkConfirmation': hasUnmarkConfirmation, - }, - ); - }, - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - SkeletonImage(imageUrl: item.image, height: 80, width: 80), - Container( - padding: - const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, - borderRadius: const BorderRadius.horizontal( - left: Radius.circular(10), - ), - ), - child: Icon( - _icon, - color: Theme.of(context).colorScheme.white, - size: 18, + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + SkeletonImage(imageUrl: item.image, height: 80, width: 80), + Container( + padding: + const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(20), ), ), - ], - ), - const SizedBox(width: 8), - Expanded( - child: SizedBox( - height: 80, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DidvanText( - item.title, - style: Theme.of(context).textTheme.bodyLarge, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - Flexible( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + child: SvgPicture.asset( + _icon, + color: Theme.of(context).colorScheme.white, + height: 18, + ), + ), + ], + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + item.title, + style: Theme.of(context).textTheme.bodyLarge, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + DidvanIcons.calendar_day_light, + size: 16, + ), + const SizedBox(width: 4), + DidvanText( + DateTime.parse(item.createdAt) + .toPersianDateStr(), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + const Spacer(), + if ((item.timeToRead ?? item.duration) != null) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( - DidvanIcons.calendar_day_light, + DidvanIcons.timer_light, size: 16, ), const SizedBox(width: 4), DidvanText( - DateTime.parse(item.createdAt) - .toPersianDateStr(), - style: Theme.of(context).textTheme.labelSmall, + item.timeToRead != null + ? '${item.timeToRead} دقیقه' + : DateTimeUtils.normalizeTimeDuration( + Duration(seconds: item.duration!), + ), + style: + Theme.of(context).textTheme.labelSmall, ), ], ), - 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, - ), - ], - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (enableCaption) - Expanded( - child: Column( - children: [ - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.edit_outlined, - size: 16, - color: Theme.of(context).colorScheme.caption, - ), - const SizedBox(width: 4), - DidvanText( - 'یادداشت‌های من', - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.caption, - ), - ], - ), - Row( - children: [ - Flexible( - child: Container( - height: 1, - color: Theme.of(context).colorScheme.primary, - ), - ), - Flexible( - flex: 2, - child: Container( - height: 1, - color: Theme.of(context).colorScheme.border, - ), - ) - ], - ), - DidvanTextField( - disableBorders: true, - initialValue: item.description, - hintText: 'برای اضافه کردن یادداشت لمس کنید.', - onChanged: (value) => UserProvider.changeItemMark( - item.type, - item.id, - null, - description: value, + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (enableCaption) + Expanded( + child: Column( + children: [ + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.edit_outlined, + size: 16, + color: Theme.of(context).colorScheme.caption, ), - isSmall: true, + const SizedBox(width: 4), + DidvanText( + 'یادداشت‌های من', + style: Theme.of(context).textTheme.labelSmall, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + Row( + children: [ + Flexible( + child: Container( + height: 1, + color: Theme.of(context).colorScheme.primary, + ), + ), + Flexible( + flex: 2, + child: Container( + height: 1, + color: Theme.of(context).colorScheme.border, + ), + ) + ], + ), + DidvanTextField( + disableBorders: true, + initialValue: item.description, + hintText: 'برای اضافه کردن یادداشت لمس کنید.', + onChanged: (value) => UserProvider.changeItemMark( + item.type, + item.id, + null, + description: value, ), - ], - ), + isSmall: true, + ), + ], ), - if (!enableCaption) const Spacer(), - if (enableBookmark) ...[ - const SizedBox( - width: 12, - ), - BookmarkButton( - value: item.marked, - onMarkChanged: (value) => onMarkChanged(item.id, value), - gestureSize: 32, - type: item.type, - itemId: item.id, - askForConfirmation: true, - ), - ], + ), + if (!enableCaption) const Spacer(), + if (enableBookmark) ...[ + const SizedBox( + width: 12, + ), + BookmarkButton( + value: item.marked, + onMarkChanged: (value) => onMarkChanged(item.id, value), + gestureSize: 32, + type: item.type, + itemId: item.id, + askForConfirmation: true, + ), ], - ), - ], - ), + ], + ), + const SizedBox(height: 10,), + Divider(color: Theme.of(context).colorScheme.border), + ], ); } diff --git a/lib/views/widgets/tag_item.dart b/lib/views/widgets/tag_item.dart index 4d2d603..b83bd87 100644 --- a/lib/views/widgets/tag_item.dart +++ b/lib/views/widgets/tag_item.dart @@ -1,5 +1,4 @@ import 'package:didvan/config/design_config.dart'; -import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/tag.dart'; import 'package:didvan/routes/routes.dart'; @@ -35,22 +34,25 @@ class TagItem extends StatelessWidget { horizontal: 8, ), decoration: BoxDecoration( - borderRadius: DesignConfig.lowBorderRadius, + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), border: Border.all( - color: Theme.of(context).colorScheme.focusedBorder, + color: const Color.fromARGB(255, 184, 184, 184), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( + const Icon( DidvanIcons.hashtag_regular, - color: Theme.of(context).colorScheme.focusedBorder, + color: Color.fromARGB(255, 102, 102, 102), + size: 17, ), DidvanText( tag.label, - color: Theme.of(context).colorScheme.focusedBorder, - style: Theme.of(context).textTheme.bodyLarge, + color: const Color.fromARGB(255, 102, 102, 102), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ), ], ),