From 1cabd1196ab4b97860238e4a4aa735cd3f22667a Mon Sep 17 00:00:00 2001 From: "Mr.Jebelli" Date: Sun, 9 Nov 2025 07:48:31 +0330 Subject: [PATCH] redesign news and radar --- lib/assets/icons/Economic_NoBG.svg | 5 + lib/assets/icons/Ejtemaei.svg | 5 + lib/assets/icons/EjtemaeiNoBg.svg | 4 + lib/assets/icons/Fanavari.svg | 15 + lib/assets/icons/FanavariNoBG.svg | 14 + lib/assets/icons/KasboKar.svg | 5 + lib/assets/icons/KasbokarNoBG.svg | 3 + lib/assets/icons/New_Economic.svg | 13 + lib/assets/icons/New_Ejtemaei.svg | 12 + lib/assets/icons/New_Fanavari.svg | 22 + lib/assets/icons/New_Kasbokar.svg | 11 + lib/assets/icons/New_Siasi.svg | 15 + lib/assets/icons/New_ZistMohit.svg | 13 + lib/assets/icons/Siasi.svg | 7 + lib/assets/icons/SiasiNoBG.svg | 7 + lib/assets/icons/ZistMohit.svg | 12 + lib/assets/icons/ZistMohitNoBG.svg | 3 + lib/assets/icons/Zist_Mohit.svg | 4 + lib/assets/icons/cpu-setting.svg | 14 + lib/assets/icons/economic.svg | 4 + lib/assets/icons/economic_bg.svg | 5 + .../icons/fluent_mention-32-regular.svg | 3 + lib/assets/icons/icon-park-outline_earth.svg | 6 + lib/assets/icons/message-text.svg | 5 + .../solar_users-group-two-rounded-broken.svg | 4 + lib/models/home_page_content/content.dart | 10 +- lib/models/news_details_data.dart | 4 + lib/models/radar_details_data.dart | 4 + lib/services/ai_rag_service.dart | 19 + lib/views/comments/comments.dart | 93 +- lib/views/direct/widgets/audio_widget.dart | 126 ++- lib/views/home/explore/explore.dart | 6 +- lib/views/home/main/main_page_state.dart | 20 +- lib/views/news/news_details/news_details.dart | 4 +- .../news/news_details/news_details_state.dart | 48 +- lib/views/profile/profile.dart | 395 +++++--- lib/views/radar/radar.dart | 581 +++++++---- .../radar/radar_details/radar_details.dart | 1 + .../radar_details/radar_details_state.dart | 48 +- lib/views/radar/radar_state.dart | 11 + lib/views/widgets/ai_chat_dialog.dart | 151 ++- lib/views/widgets/category_item.dart | 34 +- lib/views/widgets/didvan/icon_button.dart | 26 +- lib/views/widgets/didvan/page_view.dart | 914 ++++++++++++++---- .../widgets/floating_navigation_bar.dart | 55 +- lib/views/widgets/overview/news.dart | 57 +- lib/views/widgets/overview/radar.dart | 170 ++-- 47 files changed, 2213 insertions(+), 775 deletions(-) create mode 100644 lib/assets/icons/Economic_NoBG.svg create mode 100644 lib/assets/icons/Ejtemaei.svg create mode 100644 lib/assets/icons/EjtemaeiNoBg.svg create mode 100644 lib/assets/icons/Fanavari.svg create mode 100644 lib/assets/icons/FanavariNoBG.svg create mode 100644 lib/assets/icons/KasboKar.svg create mode 100644 lib/assets/icons/KasbokarNoBG.svg create mode 100644 lib/assets/icons/New_Economic.svg create mode 100644 lib/assets/icons/New_Ejtemaei.svg create mode 100644 lib/assets/icons/New_Fanavari.svg create mode 100644 lib/assets/icons/New_Kasbokar.svg create mode 100644 lib/assets/icons/New_Siasi.svg create mode 100644 lib/assets/icons/New_ZistMohit.svg create mode 100644 lib/assets/icons/Siasi.svg create mode 100644 lib/assets/icons/SiasiNoBG.svg create mode 100644 lib/assets/icons/ZistMohit.svg create mode 100644 lib/assets/icons/ZistMohitNoBG.svg create mode 100644 lib/assets/icons/Zist_Mohit.svg create mode 100644 lib/assets/icons/cpu-setting.svg create mode 100644 lib/assets/icons/economic.svg create mode 100644 lib/assets/icons/economic_bg.svg create mode 100644 lib/assets/icons/fluent_mention-32-regular.svg create mode 100644 lib/assets/icons/icon-park-outline_earth.svg create mode 100644 lib/assets/icons/message-text.svg create mode 100644 lib/assets/icons/solar_users-group-two-rounded-broken.svg diff --git a/lib/assets/icons/Economic_NoBG.svg b/lib/assets/icons/Economic_NoBG.svg new file mode 100644 index 0000000..2362653 --- /dev/null +++ b/lib/assets/icons/Economic_NoBG.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/Ejtemaei.svg b/lib/assets/icons/Ejtemaei.svg new file mode 100644 index 0000000..90a0905 --- /dev/null +++ b/lib/assets/icons/Ejtemaei.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/EjtemaeiNoBg.svg b/lib/assets/icons/EjtemaeiNoBg.svg new file mode 100644 index 0000000..d33078e --- /dev/null +++ b/lib/assets/icons/EjtemaeiNoBg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/Fanavari.svg b/lib/assets/icons/Fanavari.svg new file mode 100644 index 0000000..b4f92d5 --- /dev/null +++ b/lib/assets/icons/Fanavari.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/assets/icons/FanavariNoBG.svg b/lib/assets/icons/FanavariNoBG.svg new file mode 100644 index 0000000..fb28f17 --- /dev/null +++ b/lib/assets/icons/FanavariNoBG.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/lib/assets/icons/KasboKar.svg b/lib/assets/icons/KasboKar.svg new file mode 100644 index 0000000..7f564ff --- /dev/null +++ b/lib/assets/icons/KasboKar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/KasbokarNoBG.svg b/lib/assets/icons/KasbokarNoBG.svg new file mode 100644 index 0000000..bda4780 --- /dev/null +++ b/lib/assets/icons/KasbokarNoBG.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/New_Economic.svg b/lib/assets/icons/New_Economic.svg new file mode 100644 index 0000000..9c502c9 --- /dev/null +++ b/lib/assets/icons/New_Economic.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/lib/assets/icons/New_Ejtemaei.svg b/lib/assets/icons/New_Ejtemaei.svg new file mode 100644 index 0000000..dc1758e --- /dev/null +++ b/lib/assets/icons/New_Ejtemaei.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/lib/assets/icons/New_Fanavari.svg b/lib/assets/icons/New_Fanavari.svg new file mode 100644 index 0000000..75e3c84 --- /dev/null +++ b/lib/assets/icons/New_Fanavari.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/icons/New_Kasbokar.svg b/lib/assets/icons/New_Kasbokar.svg new file mode 100644 index 0000000..4196b15 --- /dev/null +++ b/lib/assets/icons/New_Kasbokar.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/assets/icons/New_Siasi.svg b/lib/assets/icons/New_Siasi.svg new file mode 100644 index 0000000..f1d5df9 --- /dev/null +++ b/lib/assets/icons/New_Siasi.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/assets/icons/New_ZistMohit.svg b/lib/assets/icons/New_ZistMohit.svg new file mode 100644 index 0000000..b88cf47 --- /dev/null +++ b/lib/assets/icons/New_ZistMohit.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/lib/assets/icons/Siasi.svg b/lib/assets/icons/Siasi.svg new file mode 100644 index 0000000..92ec1cb --- /dev/null +++ b/lib/assets/icons/Siasi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/assets/icons/SiasiNoBG.svg b/lib/assets/icons/SiasiNoBG.svg new file mode 100644 index 0000000..034f223 --- /dev/null +++ b/lib/assets/icons/SiasiNoBG.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/assets/icons/ZistMohit.svg b/lib/assets/icons/ZistMohit.svg new file mode 100644 index 0000000..2302fdd --- /dev/null +++ b/lib/assets/icons/ZistMohit.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/lib/assets/icons/ZistMohitNoBG.svg b/lib/assets/icons/ZistMohitNoBG.svg new file mode 100644 index 0000000..4956d2e --- /dev/null +++ b/lib/assets/icons/ZistMohitNoBG.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/Zist_Mohit.svg b/lib/assets/icons/Zist_Mohit.svg new file mode 100644 index 0000000..90dd33b --- /dev/null +++ b/lib/assets/icons/Zist_Mohit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/cpu-setting.svg b/lib/assets/icons/cpu-setting.svg new file mode 100644 index 0000000..f8c57ab --- /dev/null +++ b/lib/assets/icons/cpu-setting.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/lib/assets/icons/economic.svg b/lib/assets/icons/economic.svg new file mode 100644 index 0000000..faaacc2 --- /dev/null +++ b/lib/assets/icons/economic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/economic_bg.svg b/lib/assets/icons/economic_bg.svg new file mode 100644 index 0000000..161612a --- /dev/null +++ b/lib/assets/icons/economic_bg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/fluent_mention-32-regular.svg b/lib/assets/icons/fluent_mention-32-regular.svg new file mode 100644 index 0000000..d41c359 --- /dev/null +++ b/lib/assets/icons/fluent_mention-32-regular.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/icon-park-outline_earth.svg b/lib/assets/icons/icon-park-outline_earth.svg new file mode 100644 index 0000000..bad0e4d --- /dev/null +++ b/lib/assets/icons/icon-park-outline_earth.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/assets/icons/message-text.svg b/lib/assets/icons/message-text.svg new file mode 100644 index 0000000..64030ae --- /dev/null +++ b/lib/assets/icons/message-text.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/solar_users-group-two-rounded-broken.svg b/lib/assets/icons/solar_users-group-two-rounded-broken.svg new file mode 100644 index 0000000..bfbf923 --- /dev/null +++ b/lib/assets/icons/solar_users-group-two-rounded-broken.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/models/home_page_content/content.dart b/lib/models/home_page_content/content.dart index e32a62a..dc89c3d 100644 --- a/lib/models/home_page_content/content.dart +++ b/lib/models/home_page_content/content.dart @@ -1,13 +1,13 @@ - class MainPageContentType { final int id; final String title; final String image; final String link; bool marked; - bool isViewed; + bool isViewed; final List subtitles; final int? duration; + final String? description; MainPageContentType({ required this.id, @@ -15,9 +15,10 @@ class MainPageContentType { required this.image, required this.link, required this.marked, - required this.isViewed, + required this.isViewed, required this.subtitles, this.duration, + this.description, }); factory MainPageContentType.fromJson(Map json) => @@ -30,5 +31,6 @@ class MainPageContentType { isViewed: json['isViewed'] ?? false, subtitles: List.from(json['subtitles']), duration: json['duration'], + description: json['description'], ); -} \ No newline at end of file +} diff --git a/lib/models/news_details_data.dart b/lib/models/news_details_data.dart index cfab02d..24fe67e 100644 --- a/lib/models/news_details_data.dart +++ b/lib/models/news_details_data.dart @@ -17,6 +17,7 @@ class NewsDetailsData { final List contents; final List relatedContents = []; bool relatedContentsIsEmpty; + String? description; NewsDetailsData({ required this.id, @@ -32,6 +33,7 @@ class NewsDetailsData { required this.contents, required this.order, this.relatedContentsIsEmpty = false, + this.description, }); factory NewsDetailsData.fromJson(Map json) { @@ -46,6 +48,7 @@ class NewsDetailsData { likes: json['likes'], comments: json['comments'], order: json['order'], + description: json['description'], tags: List.from(json['tags'].map((tag) => Tag.fromJson(tag))), contents: List.from(json['contents'].map( (content) => Content.fromJson(content), @@ -64,6 +67,7 @@ class NewsDetailsData { 'liked': liked, 'likes': likes, 'comments': comments, + 'description': description, 'tags': tags.map((e) => e.toJson()).toList(), 'contents': contents.map((e) => e.toJson()).toList(), }; diff --git a/lib/models/radar_details_data.dart b/lib/models/radar_details_data.dart index 839bebb..76e8e11 100644 --- a/lib/models/radar_details_data.dart +++ b/lib/models/radar_details_data.dart @@ -21,6 +21,7 @@ class RadarDetailsData { final int order; final List relatedContents = []; bool relatedContentsIsEmpty; + String? description; RadarDetailsData({ required this.id, @@ -38,6 +39,7 @@ class RadarDetailsData { required this.order, required this.duration, this.relatedContentsIsEmpty = false, + this.description, }); factory RadarDetailsData.fromJson(Map json) { @@ -64,6 +66,7 @@ class RadarDetailsData { ), ), duration: json['duration'], + description: json['description'], ); } @@ -81,5 +84,6 @@ class RadarDetailsData { 'tags': tags.map((e) => e.toJson()).toList(), 'contents': contents.map((e) => e.toJson()).toList(), 'categories': categories.map((e) => e.toJson()).toList(), + 'description': description, }; } diff --git a/lib/services/ai_rag_service.dart b/lib/services/ai_rag_service.dart index 9124455..cf24e9a 100644 --- a/lib/services/ai_rag_service.dart +++ b/lib/services/ai_rag_service.dart @@ -6,6 +6,21 @@ class AiRagService { 'https://n8n-didvan.liara.run/webhook/didvan-rag'; static const String _authHeader = 'Basic dXNlcjpkYXNqZHBuM3BjdTQzcDM0aWpyaA=='; + static const String _didvanBaseUrl = 'https://api.didvan.app'; + + static String _getFullAudioUrl(String? url) { + if (url == null || url.isEmpty) return ''; + + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + if (url.startsWith('/')) { + return '$_didvanBaseUrl$url'; + } + + return '$_didvanBaseUrl/$url'; + } static Future sendMessage(String message) async { try { @@ -23,9 +38,11 @@ class AiRagService { if (response.statusCode == 200) { final data = jsonDecode(response.body); + final rawAudioUrl = data['url'] as String?; return AiRagResponse( output: data['output'] ?? '', sources: List.from(data['sources'] ?? []), + audioUrl: _getFullAudioUrl(rawAudioUrl), isSuccess: true, ); } else { @@ -48,11 +65,13 @@ class AiRagService { class AiRagResponse { final String output; final List sources; + final String? audioUrl; final bool isSuccess; AiRagResponse({ required this.output, required this.sources, + this.audioUrl, required this.isSuccess, }); } diff --git a/lib/views/comments/comments.dart b/lib/views/comments/comments.dart index 59596bf..3f9af75 100644 --- a/lib/views/comments/comments.dart +++ b/lib/views/comments/comments.dart @@ -55,46 +55,65 @@ class _CommentsState extends State { } } _bottomPadding = bottomViewInset; - return Material( - child: DidvanScaffold( - hidePlayer: true, - physics: const BouncingScrollPhysics(), - backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: _isPage - ? AppBarData( - hasBack: true, - title: 'نظرات', - subtitle: widget.pageData['title'], - ) - : null, - padding: _isPage ? const EdgeInsets.only(bottom: 92) : EdgeInsets.zero, - slivers: [ - Consumer( - builder: (context, state, child) => - SliverStateHandler( - onRetry: state.getComments, - state: state, - itemPadding: const EdgeInsets.only(top: 16, bottom: 16), - childCount: state.comments.length, - placeholder: const _CommentPlaceholder(), - centerEmptyState: _isPage, - enableEmptyState: state.comments.isEmpty, - emptyState: EmptyState( - asset: Assets.emptyChat, - title: 'اولین نظر را بنویسید...', - ), - builder: (context, state, index) => Comment( - key: ValueKey( - state.comments[index].id.toString() + - state.comments[index].text, - ), - focusNode: _focusNode, - comment: state.comments[index], + + final scaffold = DidvanScaffold( + hidePlayer: true, + physics: const BouncingScrollPhysics(), + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: _isPage + ? AppBarData( + hasBack: true, + title: 'نظرات', + subtitle: widget.pageData['title'], + ) + : null, + padding: _isPage ? const EdgeInsets.only(bottom: 92) : EdgeInsets.zero, + slivers: [ + Consumer( + builder: (context, state, child) => SliverStateHandler( + onRetry: state.getComments, + state: state, + itemPadding: const EdgeInsets.only(top: 16, bottom: 16), + childCount: state.comments.length, + placeholder: const _CommentPlaceholder(), + centerEmptyState: _isPage, + enableEmptyState: state.comments.isEmpty, + emptyState: EmptyState( + asset: Assets.emptyChat, + title: 'اولین نظر را بنویسید...', + ), + builder: (context, state, index) => Comment( + key: ValueKey( + state.comments[index].id.toString() + + state.comments[index].text, ), + focusNode: _focusNode, + comment: state.comments[index], ), ), - ], - ), + ), + ], + ); + + if (_isPage) { + return Scaffold( + resizeToAvoidBottomInset: true, + body: Stack( + children: [ + Positioned.fill(child: scaffold), + Positioned( + left: 20, + right: 20, + bottom: 20, + child: CommentMessageBox(focusNode: _focusNode), + ), + ], + ), + ); + } + + return Material( + child: scaffold, ); } } diff --git a/lib/views/direct/widgets/audio_widget.dart b/lib/views/direct/widgets/audio_widget.dart index 67938ec..3c92884 100644 --- a/lib/views/direct/widgets/audio_widget.dart +++ b/lib/views/direct/widgets/audio_widget.dart @@ -5,7 +5,7 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/studio_details_data.dart'; import 'package:didvan/services/media/media.dart'; -import 'package:didvan/views/widgets/audio/audio_slider.dart'; +import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart'; import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:flutter/material.dart'; @@ -27,41 +27,103 @@ class AudioWidget extends StatelessWidget { this.duration, }) : super(key: key); + 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 "$twoDigitMinutes:$twoDigitSeconds"; + } + @override Widget build(BuildContext context) { - return StreamBuilder( - stream: MediaService.audioPlayer.playingStream, + final tag = + audioMetaData != null ? '${audioMetaData!.type}-$id' : 'message-$id'; + + return StreamBuilder( + stream: MediaService.audioPlayer.playerStateStream, builder: (context, snapshot) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 12), - child: Row( - children: [ - Expanded( - child: AudioSlider( - tag: audioMetaData != null - ? '${audioMetaData!.type}-$id' - : 'message-$id', - duration: - audioMetaData?.duration ?? duration?.inSeconds, - showTimer: true, - ), - ), - ], + final isCurrentlyPlaying = MediaService.audioPlayerTag == tag; + + return StreamBuilder( + stream: MediaService.audioPlayer.positionStream, + builder: (context, posSnapshot) { + var totalDuration = audioMetaData?.duration != null + ? Duration(seconds: audioMetaData!.duration!) + : (duration ?? Duration.zero); + + if (isCurrentlyPlaying && + MediaService.audioPlayer.duration != null && + MediaService.audioPlayer.duration! > Duration.zero) { + totalDuration = MediaService.audioPlayer.duration!; + } + + final currentPosition = isCurrentlyPlaying + ? (posSnapshot.data ?? Duration.zero) + : Duration.zero; + + final progress = (totalDuration.inMilliseconds > 0) + ? currentPosition.inMilliseconds / totalDuration.inMilliseconds + : 0.0; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _AudioControllerButton( + audioFile: audioFile, + audioUrl: audioUrl, + id: id, + audioMetaData: audioMetaData, ), - ), - ), - const SizedBox(width: 12), - _AudioControllerButton( - audioFile: audioFile, - audioUrl: audioUrl, - id: id, - audioMetaData: audioMetaData, - ), - ], + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + SizedBox( + width: 45, + child: Text( + _formatDuration(totalDuration), + style: const TextStyle( + fontSize: 12, + color: Color.fromARGB(255, 102, 102, 102)), + textAlign: TextAlign.center, + ), + ), + const SizedBox(width: 8), + Expanded( + child: AudioWaveformProgress( + progress: progress.clamp(0.0, 1.0), + isActive: isCurrentlyPlaying, + onChanged: isCurrentlyPlaying + ? (value) { + final seekMillis = + (value * totalDuration.inMilliseconds) + .toInt(); + MediaService.audioPlayer.seek( + Duration(milliseconds: seekMillis)); + } + : null, + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 45, + child: Text( + _formatDuration(currentPosition), + style: const TextStyle( + fontSize: 12, + color: Color.fromARGB(255, 102, 102, 102)), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ], + ); + }, ); }, ); diff --git a/lib/views/home/explore/explore.dart b/lib/views/home/explore/explore.dart index 681e587..00bc26c 100644 --- a/lib/views/home/explore/explore.dart +++ b/lib/views/home/explore/explore.dart @@ -178,7 +178,7 @@ class StaggeredEntry extends StatefulWidget { super.key, required this.child, required this.index, - this.duration = const Duration(milliseconds: 500), + this.duration = const Duration(milliseconds: 250), this.verticalOffset = 50.0, }); @@ -268,7 +268,7 @@ class SwotSection extends StatelessWidget { ), const SizedBox(width: 5), DidvanText( - "باید ها و نباید ها", + "بایدها و نبایدها", style: Theme.of(context).textTheme.titleSmall, color: const Color.fromARGB(255, 0, 89, 119), ), @@ -529,6 +529,7 @@ class MainPageSection extends StatelessWidget { list.type, item.id, item.link, + description: item.description, ), child: Padding( padding: const EdgeInsets.all(8.0), @@ -605,6 +606,7 @@ class MainPageSection extends StatelessWidget { list.type, item.id, item.link, + description: item.description, ), child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart index b8eb775..89ec7ef 100644 --- a/lib/views/home/main/main_page_state.dart +++ b/lib/views/home/main/main_page_state.dart @@ -24,27 +24,22 @@ class MainPageState extends CoreProvier { List swotItems = []; Future _getMainPageContent() async { - print("DEBUG: _getMainPageContent started"); final service = RequestService(RequestHelper.mainPageContent); await service.httpGet(); if (service.isSuccess) { - print("DEBUG: _getMainPageContent success"); content = MainPageContent.fromJson(service.result); - print("DEBUG: _getMainPageContent service.result: ${service.result}"); unread = service.result['unread']; - print("DEBUG: __getMainPageContent unread: $unread, content: $content"); notifyListeners(); } else { - print("DEBUG: _getMainPageContent failed state"); notifyListeners(); throw Exception('Failed to load main page content'); } } int getStoryStartIndex(List stories) { - final firstUnreadIndex = stories.indexWhere((story) => !story.isViewed); - return firstUnreadIndex != -1 ? firstUnreadIndex : 0; -} + final firstUnreadIndex = stories.indexWhere((story) => !story.isViewed); + return firstUnreadIndex != -1 ? firstUnreadIndex : 0; + } Future _getSwotItems() async { try { @@ -94,8 +89,9 @@ class MainPageState extends CoreProvier { void navigationHandler( String type, int id, - String? link, - ) { + String? link, { + String? description, + }) { link = link ?? ''; dynamic args; switch (type) { @@ -118,6 +114,7 @@ class MainPageState extends CoreProvier { 'id': id, 'args': const NewsRequestArgs(page: 0), 'hasUnmarkConfirmation': false, + 'description': description, }; break; } @@ -129,6 +126,7 @@ class MainPageState extends CoreProvier { 'id': id, 'args': const RadarRequestArgs(page: 0), 'hasUnmarkConfirmation': false, + 'description': description, }; break; } @@ -155,4 +153,4 @@ class MainPageState extends CoreProvier { } Navigator.of(navigatorKey.currentContext!).pushNamed(link, arguments: args); } -} \ No newline at end of file +} diff --git a/lib/views/news/news_details/news_details.dart b/lib/views/news/news_details/news_details.dart index d6a021e..6329b49 100644 --- a/lib/views/news/news_details/news_details.dart +++ b/lib/views/news/news_details/news_details.dart @@ -28,6 +28,7 @@ class _NewsDetailsState extends State { } }); state.args = widget.pageData['args'] ?? const NewsRequestArgs(page: 0); + state.initialDescription = widget.pageData['description']; Future.delayed(Duration.zero, () { state.getNewsDetails(widget.pageData['id']); }); @@ -42,8 +43,7 @@ class _NewsDetailsState extends State { return Scaffold( body: Consumer( builder: (context, state, child) => StateHandler( - onRetry: () => - state.getNewsDetails(widget.pageData['id']), + onRetry: () => state.getNewsDetails(widget.pageData['id']), state: state, builder: (context, state) => Stack( children: [ diff --git a/lib/views/news/news_details/news_details_state.dart b/lib/views/news/news_details/news_details_state.dart index c4c2dea..65a16a5 100644 --- a/lib/views/news/news_details/news_details_state.dart +++ b/lib/views/news/news_details/news_details_state.dart @@ -13,6 +13,7 @@ class NewsDetailsState extends CoreProvier { final List news = []; late final int initialIndex; late final NewsRequestArgs args; + String? initialDescription; late Timer _trackingTimer; int _trackingTimerCounter = 0; bool isFetchingNewItem = false; @@ -26,13 +27,11 @@ class NewsDetailsState extends CoreProvier { NewsDetailsData get currentNews => news[_currentIndex]!; Future getNewsDetails(int id, {bool? isForward}) async { - // Prevent duplicate requests if (_isRequestInProgress) { return; } _isRequestInProgress = true; - // Set loading state if (isForward == null) { appState = AppState.busy; } else { @@ -41,7 +40,6 @@ class NewsDetailsState extends CoreProvier { } try { - // Wait for token to be ready if (RequestService.token == null || RequestService.token!.isEmpty) { await Future.delayed(const Duration(milliseconds: 500)); if (RequestService.token == null || RequestService.token!.isEmpty) { @@ -56,6 +54,15 @@ class NewsDetailsState extends CoreProvier { if (service.isSuccess) { final result = service.result; final newsItem = NewsDetailsData.fromJson(result['news']); + + if (isForward == null && initialDescription != null) { + newsItem.description = initialDescription; + } + + if (isForward == null && newsItem.description == null) { + await _fetchDescriptionFromList(newsItem.id, newsItem); + } + if (args.page == 0) { news.add(newsItem); initialIndex = 0; @@ -73,7 +80,8 @@ class NewsDetailsState extends CoreProvier { nextNews = NewsDetailsData.fromJson(result['nextNews']); } if (isForward == null) { - news.addAll(List.generate(max(newsItem.order - 2, 0), (index) => null)); + news.addAll( + List.generate(max(newsItem.order - 2, 0), (index) => null)); if (prevNews != null) { news.add(prevNews); } @@ -107,6 +115,7 @@ class NewsDetailsState extends CoreProvier { } } } catch (e) { + // ignore: avoid_print print('Error fetching news details: $e'); isFetchingNewItem = false; if (isForward == null) { @@ -145,6 +154,35 @@ class NewsDetailsState extends CoreProvier { _trackingTimer.cancel(); } + Future _fetchDescriptionFromList( + int newsId, NewsDetailsData newsItem) async { + try { + final service = RequestService( + RequestHelper.newsOverviews( + args: const NewsRequestArgs(page: 1), + ), + ); + await service.httpGet(); + + if (service.isSuccess) { + final newsList = service.result['news'] as List?; + if (newsList != null) { + for (var item in newsList) { + if (item['id'] == newsId) { + final newsData = OverviewData.fromJson(item); + if (newsData.description.isNotEmpty) { + newsItem.description = newsData.description; + } + break; + } + } + } + } + } catch (e) { + print('Error fetching description from list: $e'); + } + } + Future getRelatedContents() async { if (currentNews.relatedContents.isNotEmpty && currentNews.relatedContentsIsEmpty) { @@ -187,4 +225,4 @@ class NewsDetailsState extends CoreProvier { _trackingTimer.cancel(); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/views/profile/profile.dart b/lib/views/profile/profile.dart index 51e7052..6b54610 100644 --- a/lib/views/profile/profile.dart +++ b/lib/views/profile/profile.dart @@ -3,7 +3,6 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; -import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/providers/theme.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; @@ -54,164 +53,276 @@ class _ProfilePageState extends State { @override Widget build(BuildContext context) { + final user = context.watch().user; + return Consumer( builder: (context, state, child) => StateHandler( onRetry: context.read().init, state: context.read(), builder: (context, state) => DidvanScaffold( - padding: const EdgeInsets.all(16), - appBarData: AppBarData(title: 'تنظیمات', hasBack: true), + padding: const EdgeInsets.fromLTRB(8, 20, 0, 20), + appBarData: null, + showSliversFirst: true, + slivers: [ + SliverAppBar( + pinned: true, + backgroundColor: Theme.of(context).colorScheme.surface, + automaticallyImplyLeading: false, + leadingWidth: 200, + leading: Padding( + padding: const EdgeInsetsDirectional.only(start: 0.0), + child: SvgPicture.asset( + Assets.horizontalLogoWithText, + fit: BoxFit.contain, + height: 80, + ), + ), + actions: [ + IconButton( + onPressed: () { + Navigator.of(context).pushNamed(Routes.bookmarks); + }, + icon: SvgPicture.asset( + 'lib/assets/icons/hugeicons_telescope-01.svg')), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: SvgPicture.asset( + 'lib/assets/icons/arrow-left.svg', + color: const Color.fromARGB(255, 102, 102, 102), + )), + const SizedBox(width: 8), + ], + ), + ], children: [ - DidvanCard( + const SizedBox(height: 24), + Center( child: Column( + mainAxisSize: MainAxisSize.min, children: [ - MenuOption( - title: 'ویرایش پروفایل', - icon: DidvanIcons.user_edit_regular, - suffix: context.watch().user.fullName, - onTap: () => Navigator.of(context) - .pushNamed(Routes.editProfile), + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + border: Border.all( + color: const Color.fromARGB(255, 184, 184, 184), + width: 1.0, + ), + image: + (user.photo != null && user.photo!.isNotEmpty) + ? DecorationImage( + image: NetworkImage(user.photo!), + fit: BoxFit.cover, + ) + : null, + ), + alignment: Alignment.center, + child: (user.photo == null || user.photo!.isEmpty) + ? Icon( + Icons.person_rounded, + size: 60, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withOpacity(0.5), + ) + : null, ), - const DidvanDivider(), - MenuOption( - title: 'تنظیمات', - icon: DidvanIcons.setting_regular, - onTap: () { - state.showSettings = !state.showSettings; - state.update(); - }, - trailing: Icon( - state.showSettings - ? DidvanIcons.angle_up_regular - : DidvanIcons.angle_down_regular, - size: 18, - color: Theme.of(context).colorScheme.title, - )), - AnimatedVisibility( - duration: DesignConfig.lowAnimationDuration, - isVisible: state.showSettings, - child: Padding( - padding: const EdgeInsets.only(right: 8.0, top: 8), - child: Column( - children: [ - // Padding( - // padding: const EdgeInsets.symmetric( - // vertical: 12.0), - // child: MenuOption( - // title: 'زمان دریافت اعلان', - // onTap: () => - // Navigator.of(context).pushNamed( - // Routes.notificationTime, - // arguments: { - // "fromFav": false, - // 'onTimeChanged': () => Future.delayed( - // Duration.zero, - // () => state.getTime(), - // ) - // }, - // ), - // icon: DidvanIcons.notification_regular, - // suffix: state.time, - // // suffix: 'از${DateTimeUtils.normalizeTimeDuration( - // // Duration(minutes: state.notificationTimeRange[0]), - // // )} تا ${DateTimeUtils.normalizeTimeDuration( - // // Duration(minutes: state.notificationTimeRange[1]), - // // )}', - // ), - // ), - // Padding( - // padding: const EdgeInsets.symmetric( - // vertical: 12.0), - // child: MenuOption( - // title: 'شخصی سازی محتوا', - // onTap: () => Navigator.of(context) - // .pushNamed(Routes.favouritesStep, - // arguments: {"toTimer": false}), - // icon: DidvanIcons.note_regular, - // // suffix: 'از${DateTimeUtils.normalizeTimeDuration( - // // Duration(minutes: state.notificationTimeRange[0]), - // // )} تا ${DateTimeUtils.normalizeTimeDuration( - // // Duration(minutes: state.notificationTimeRange[1]), - // // )}', - // ), - // ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0), - child: MenuOption( - suffix: state.fontFamily == 'Dana-FA' - ? 'دانا' - : state.fontFamily == 'Iransans-FA' - ? 'ایران سنس' - : 'ایران یکان', - title: 'فونت برنامه', - onTap: _showFontFamilyBottomSheet, - icon: DidvanIcons.font_regular, - ), - ), - Padding( + const SizedBox(height: 4), + const SizedBox(height: 12), + // ویجت نمایش نام کاربر + DidvanText( + user.fullName, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + color: + const Color.fromARGB(255, 0, 126, 167)), + ), + // نمایش شماره تلفن (اختیاری) + // const SizedBox(height: 4), + // Directionality( + // textDirection: TextDirection.ltr, + // child: DidvanText( + // user.phoneNumber, + // style: Theme.of(context) + // .textTheme + // .bodyMedium + // ?.copyWith( + // color: Theme.of(context) + // .colorScheme + // .onSurface + // .withOpacity(0.6), + // ), + // ), + // ), + ], + ), + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.all(8.0), + child: DidvanCard( + child: Column( + children: [ + MenuOption( + title: 'ویرایش پروفایل', + icon: DidvanIcons.user_edit_regular, + // suffix: context.watch().user.fullName, + onTap: () => Navigator.of(context) + .pushNamed(Routes.editProfile), + ), + const DidvanDivider(), + // ... بقیه کدها بدون تغییر + MenuOption( + title: 'تنظیمات', + icon: DidvanIcons.setting_regular, + onTap: () { + state.showSettings = !state.showSettings; + state.update(); + }, + trailing: Icon( + state.showSettings + ? DidvanIcons.angle_up_regular + : DidvanIcons.angle_down_regular, + size: 18, + color: Theme.of(context).colorScheme.title, + )), + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: state.showSettings, + child: Padding( + padding: + const EdgeInsets.only(right: 8.0, top: 8), + child: Column( + children: [ + Padding( padding: const EdgeInsets.symmetric( vertical: 12.0), child: MenuOption( - suffix: - fontScaleSuffix(state.fontSizeScale), - title: 'اندازه متن', - onTap: _showFontScaleBottomSheet, - icon: DidvanIcons.font_size_solid, - )), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0), - child: MenuOption( - title: 'ظاهر برنامه', - onTap: () { - state.showThemes = !state.showThemes; - state.update(); + title: 'زمان دریافت اعلان', + onTap: () => + Navigator.of(context).pushNamed( + Routes.notificationTime, + arguments: { + "fromFav": false, + 'onTimeChanged': () => Future.delayed( + Duration.zero, + () => state.getTime(), + ) }, - icon: DidvanIcons.theme_regular, - trailing: Icon( - state.showThemes - ? DidvanIcons.angle_up_regular - : DidvanIcons.angle_down_regular, - size: 18, - color: Theme.of(context) - .colorScheme - .title, - ))), - AnimatedVisibility( - duration: DesignConfig.lowAnimationDuration, - isVisible: state.showThemes, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _themeItem(context, state, 'light'), - _themeItem(context, state, 'dark'), - ], + ), + icon: DidvanIcons.notification_regular, + suffix: state.time, + // suffix: + // 'از${DateTimeUtils.normalizeTimeDuration( + // Duration( + // minutes: + // state.notificationTimeRange[0]), + // )} تا ${DateTimeUtils.normalizeTimeDuration( + // Duration( + // minutes: + // state.notificationTimeRange[1]), + // )}', ), ), - ), - ], + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: MenuOption( + title: 'شخصی سازی محتوا', + onTap: () => Navigator.of(context) + .pushNamed(Routes.favouritesStep, + arguments: {"toTimer": false}), + icon: DidvanIcons.note_regular, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: MenuOption( + suffix: state.fontFamily == 'Dana-FA' + ? 'دانا' + : state.fontFamily == 'Iransans-FA' + ? 'ایران سنس' + : 'ایران یکان', + title: 'فونت برنامه', + onTap: _showFontFamilyBottomSheet, + icon: DidvanIcons.font_regular, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: MenuOption( + suffix: fontScaleSuffix( + state.fontSizeScale), + title: 'اندازه متن', + onTap: _showFontScaleBottomSheet, + icon: DidvanIcons.font_size_solid, + )), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: MenuOption( + title: 'ظاهر برنامه', + onTap: () { + state.showThemes = + !state.showThemes; + state.update(); + }, + icon: DidvanIcons.theme_regular, + trailing: Icon( + state.showThemes + ? DidvanIcons.angle_up_regular + : DidvanIcons + .angle_down_regular, + size: 18, + color: Theme.of(context) + .colorScheme + .title, + ))), + AnimatedVisibility( + duration: DesignConfig.lowAnimationDuration, + isVisible: state.showThemes, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _themeItem(context, state, 'light'), + _themeItem(context, state, 'dark'), + ], + ), + ), + ), + ], + ), ), ), - ), - const DidvanDivider(), - MenuOption( - title: 'خروج از حساب کاربری', - icon: DidvanIcons.sign_out_regular, - color: Theme.of(context).colorScheme.secondary, - onTap: () async { - StorageService.delete(key: 'token'); - Navigator.of(context).pushNamedAndRemoveUntil( - Routes.splash, - (_) => false, - ); - }, - ), - ], + const DidvanDivider(), + MenuOption( + title: 'خروج از حساب کاربری', + icon: DidvanIcons.sign_out_regular, + color: Theme.of(context).colorScheme.secondary, + onTap: () async { + StorageService.delete(key: 'token'); + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.splash, + (_) => false, + ); + }, + ), + ], + ), ), ), const ItemTitle( diff --git a/lib/views/radar/radar.dart b/lib/views/radar/radar.dart index 21b93a1..248fafb 100644 --- a/lib/views/radar/radar.dart +++ b/lib/views/radar/radar.dart @@ -1,231 +1,341 @@ import 'dart:async'; import 'dart:math'; -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/category.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; -import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/views/radar/radar_state.dart'; -import 'package:didvan/views/widgets/categories_gird.dart'; -import 'package:didvan/views/widgets/categories_list.dart'; import 'package:didvan/views/widgets/date_picker_button.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/widgets/overview/radar.dart'; -import 'package:didvan/views/widgets/search_field.dart'; -import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/checkbox.dart'; -import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/item_title.dart'; import 'package:didvan/views/widgets/state_handlers/empty_result.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:didvan/views/widgets/home_app_bar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; class Radar extends StatefulWidget { const Radar({Key? key}) : super(key: key); @override - State createState() => _RadarState(); + State createState() => _RadarStateView(); } -class _RadarState extends State { - final ScrollController _scrollController = ScrollController(); - final _focusNode = FocusNode(); - - bool _isAnimating = false; - +class _RadarStateView extends State { Timer? _timer; + final _focusNode = FocusNode(); @override void initState() { - _scrollController.addListener(() { - _handleAnimations(); - }); - final state = context.read(); - state.addListener(() { - if (state.shouldColapse && mounted) { - _handleAnimations(true); - state.shouldColapse = false; - } - }); - state.init(); + context.read().init(); super.initState(); } + String _getCategoryIcon(int categoryId, bool isDark) { + switch (categoryId) { + case 1: + return 'lib/assets/icons/New_Economic.svg'; + case 2: + return 'lib/assets/icons/New_Siasi.svg'; + case 3: + return 'lib/assets/icons/New_Fanavari.svg'; + case 4: + return 'lib/assets/icons/New_Kasbokar.svg'; + case 5: + return 'lib/assets/icons/New_ZistMohit.svg'; + case 6: + return 'lib/assets/icons/New_Ejtemaei.svg'; + default: + return 'lib/assets/icons/Kasb o Kar.svg'; + } + } + @override Widget build(BuildContext context) { - return Consumer( - builder: (context, state, child) => DidvanScaffold( - padding: EdgeInsets.zero, - appBarData: AppBarData( - hasBack: true, - hasElevation: false, - title: 'پویش‌های افق', + final state = context.watch(); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final isDark = theme.brightness == Brightness.dark; + final showCategories = !state.filtering && !state.searching; + + return DidvanScaffold( + padding: EdgeInsets.zero, + appBarData: null, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 12.0), + child: HomeAppBar( + showBackButton: false, + showSearchField: state.appState != AppState.failed, + onSearchChanged: _onChanged, + onFilterPressed: _showFilterBottomSheet, + searchFocusNode: _focusNode, + isFiltered: state.filtering, + searchValue: state.search, + ), + ), ), - children: [ - SizedBox( - height: MediaQuery.of(context).size.height, - child: Stack( - children: [ - CustomScrollView( - physics: _isAnimating - ? const NeverScrollableScrollPhysics() - : const ClampingScrollPhysics(), - controller: _scrollController, - slivers: [ - if (!state.filtering && - !state.searching && - state.appState != AppState.failed) - const SliverToBoxAdapter( - child: SizedBox(height: 320), - ), - if (state.appState != AppState.failed) - SliverPadding( - padding: const EdgeInsets.only( - right: 16, bottom: 20, top: 20), - sliver: SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerRight, - child: AnimatedVisibility( - isVisible: !state.isColapsed && - !state.searching && - !state.filtering, - duration: DesignConfig.lowAnimationDuration, - child: DidvanText( - 'آخرین رصد', - style: Theme.of(context).textTheme.titleMedium, - color: Theme.of(context).colorScheme.title, - ), - ), - ), - ), - ), - if (state.isColapsed || state.searching || state.filtering) - const SliverToBoxAdapter( - child: SizedBox( - height: 160, - ), - ), - SliverStateHandler( - onRetry: () => state.getRadars(page: state.page), - state: state, - itemPadding: const EdgeInsets.only( - bottom: 20, - left: 16, - right: 16, - ), - enableEmptyState: state.radars.isEmpty, - emptyState: Padding( - padding: const EdgeInsets.only(bottom: 120), - child: EmptyResult( - onNewSearch: () => _focusNode.requestFocus()), - ), - placeholder: RadarOverview.placeholder, - builder: (context, state, index) { - index += 2; - if (index % 15 == 0 && state.lastPage != state.page) { - state.getRadars(page: state.page + 1); - } - index -= 2; - if (index >= state.radars.length) { - return RadarOverview.placeholder; - } - final radar = state.radars[index]; - return RadarOverview( - radar: radar, - onMarkChanged: state.changeMark, - onLikedChanged: state.changeLike, - onCommentsChanged: (id, count) => - state.onCommentsChanged(id, count), - radarRequestArgs: RadarRequestArgs( - page: state.page, - categories: List.from( - state.selectedCats.map((cat) => cat.id)), - endDate: state.endDate, - isSingleItem: false, - search: state.search, - startDate: state.startDate, - ), - ); - }, - childCount: state.radars.length + - (state.lastPage == state.page ? 0 : 3), - ), - if (state.radars.length == 1) - const SliverToBoxAdapter( - child: SizedBox(height: 320), - ), - const SliverToBoxAdapter( - child: SizedBox( - height: 100, - ), - ), - ], + SliverAppBar( + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: colorScheme.surface, + elevation: 0, + centerTitle: false, + titleSpacing: 16, + title: Text( + 'پویش‌ افق', + style: theme.textTheme.headlineSmall?.copyWith( + color: isDark + ? Colors.white + : const Color.fromARGB(255, 0, 53, 70), + fontWeight: FontWeight.bold, + fontSize: 19), + ), + actions: [ + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: SvgPicture.asset( + 'lib/assets/icons/arrow-left.svg', + width: 30, + height: 30, + colorFilter: ColorFilter.mode( + isDark + ? Colors.white70 + : const Color.fromARGB(255, 102, 102, 102), + BlendMode.srcIn), + ), + ), + const SizedBox(width: 8), + ], + ), + const SliverToBoxAdapter( + child: SizedBox( + height: 16, + ), + ), + SliverToBoxAdapter( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition( + sizeFactor: animation, + axisAlignment: -1.0, + child: FadeTransition( + opacity: animation, + child: child, ), - if (state.appState != AppState.failed) - CategoriesRow1( - topPadding: 212, - rightPadding: 124, - onSelected: _onCategorySelected, - categories: state.categories, - isColapsed: - state.isColapsed || state.searching || state.filtering, - ), - if (state.appState != AppState.failed) - CategoriesRow2( - categories: state.categories, - isColapsed: - state.isColapsed || state.searching || state.filtering, - onSelected: _onCategorySelected, - ), - if (state.appState != AppState.failed && - !state.searching && - !state.filtering) - CategoriesList( - top: 60, - categories: state.categories, - isColapsed: - state.isColapsed || state.searching || state.filtering, - onSelected: (_) => state.getRadars(page: 1), - selectedCats: state.selectedCats, - ), - if (state.appState != AppState.failed) - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(12), - ), - ), - child: SearchField( - focusNode: _focusNode, - isFiltered: state.filtering, - title: 'پویش‌های افق', - onChanged: _onChanged, - onFilterButtonPressed: _showFilterBottomSheet, - ), + ); + }, + child: showCategories + ? Column( + key: const ValueKey('categories_column'), + children: [ + if (state.categories.length >= 3) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: state.categories + .sublist(0, 3) + .map((category) => _buildCoolCategoryItem( + category, isDark, state, theme)) + .toList(), + ), + ), + if (state.categories.length >= 6) + Padding( + padding: const EdgeInsets.only( + top: 12, left: 20, right: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: state.categories + .sublist(3, 6) + .map((category) => _buildCoolCategoryItem( + category, isDark, state, theme)) + .toList(), + ), + ), + const SizedBox(height: 20), + ], + ) + : const SizedBox.shrink(key: ValueKey('empty_categories')), + ), + ), + SliverStateHandler( + centerEmptyState: false, + onRetry: () => state.getRadars(page: state.page), + state: state, + childCount: min(state.visibleCount, state.radars.length) + + (_hasMoreItems(state) ? 1 : 0), + builder: (context, state, index) { + final currentDisplayCount = + min(state.visibleCount, state.radars.length); + + if (index == currentDisplayCount && _hasMoreItems(state)) { + return _buildLoadMoreButton(context, state); + } + + if (index >= state.radars.length) { + return _SlideFadeTransition( + index: index, child: RadarOverview.placeholder); + } + + final radar = state.radars[index]; + return _SlideFadeTransition( + index: index, + child: RadarOverview( + radar: radar, + onMarkChanged: state.changeMark, + onLikedChanged: state.changeLike, + onCommentsChanged: (id, count) => + state.onCommentsChanged(id, count), + radarRequestArgs: RadarRequestArgs( + page: state.page, + categories: + List.from(state.selectedCats.map((cat) => cat.id)), + endDate: state.endDate, + isSingleItem: false, + search: state.search, + startDate: state.startDate, + ), + ), + ); + }, + enableEmptyState: + state.radars.isEmpty && state.appState != AppState.busy, + emptyState: EmptyResult( + onNewSearch: () => _focusNode.requestFocus(), + ), + itemPadding: const EdgeInsets.only( + bottom: 20, + left: 16, + right: 16, + ), + placeholder: + _SlideFadeTransition(index: 0, child: RadarOverview.placeholder), + ), + ], + ); + } + + bool _hasMoreItems(RadarState state) { + return state.radars.length > state.visibleCount || + state.page < state.lastPage; + } + + Widget _buildLoadMoreButton(BuildContext context, RadarState state) { + if (state.appState == AppState.busy && + state.radars.length <= state.visibleCount) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0), + child: Center( + child: GestureDetector( + onTap: () { + state.loadMore(); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 50.0, + vertical: 12.0, + ), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 0, 126, 167), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + 'lib/assets/icons/element-plus.svg', + colorFilter: + const ColorFilter.mode(Colors.white, BlendMode.srcIn), + width: 20, + ), + const SizedBox(width: 8), + const Text( + 'بارگذاری بیشتر', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, ), + ), ], ), ), - ], + ), ), ); } - void _onCategorySelected(CategoryData category) { - final state = context.read(); - state.selectedCats.clear(); - if (category.id != 0) { - state.selectedCats.add(category); - } - state.getRadars(page: 1); + Widget _buildCoolCategoryItem( + CategoryData category, bool isDark, RadarState state, ThemeData theme) { + final isSelected = + state.selectedCats.any((element) => element.id == category.id); + category.asset = _getCategoryIcon(category.id, isDark); + + return Expanded( + child: GestureDetector( + onTap: () => _onCategorySelected(category), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primary.withOpacity(0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: isSelected + ? theme.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: SvgPicture.asset( + category.asset!, + width: 100, + height: 100, + ), + ), + ), + const SizedBox(height: 8), + Text( + category.label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: isSelected ? FontWeight.bold : FontWeight.bold, + fontSize: 15, + ), + ), + ], + ), + ), + ); } void _onChanged(String value) { @@ -240,35 +350,19 @@ class _RadarState extends State { }); } - void _handleAnimations([bool forceAnimate = false]) async { + void _onCategorySelected(CategoryData category) { final state = context.read(); - if (_isAnimating || state.filtering || state.searching) return; - final double position = _scrollController.offset; - if (position > 5 && !state.isColapsed || forceAnimate) { - state.isScrolled = true; - _isAnimating = true; - setState(() {}); - await _scrollController.animateTo( - 380, - duration: DesignConfig.mediumAnimationDuration, - curve: Curves.easeIn, - ); - _isAnimating = false; - setState(() {}); - } else if (position < - min(_scrollController.position.maxScrollExtent, 380) && - state.isColapsed) { - state.isScrolled = false; - _isAnimating = true; - setState(() {}); - await _scrollController.animateTo( - 0, - duration: DesignConfig.mediumAnimationDuration, - curve: Curves.easeIn, - ); - _isAnimating = false; - setState(() {}); + + final isAlreadySelected = + state.selectedCats.any((cat) => cat.id == category.id); + + state.selectedCats.clear(); + + if (!isAlreadySelected && category.id != 0) { + state.selectedCats.add(category); } + + state.getRadars(page: 1); } Future _showFilterBottomSheet() async { @@ -324,13 +418,15 @@ class _RadarState extends State { width: (MediaQuery.of(context).size.width - 40) / 2, child: DidvanCheckbox( title: state.categories[i].label, - value: state.selectedCats.contains(state.categories[i]), + value: state.selectedCats + .any((cat) => cat.id == state.categories[i].id), onChanged: (value) { if (value) { state.selectedCats.add(state.categories[i]); - return; + } else { + state.selectedCats.removeWhere( + (cat) => cat.id == state.categories[i].id); } - state.selectedCats.remove(state.categories[i]); }, ), ), @@ -345,7 +441,64 @@ class _RadarState extends State { @override void dispose() { _focusNode.dispose(); - _scrollController.dispose(); super.dispose(); } } + +class _SlideFadeTransition extends StatefulWidget { + final Widget child; + final int index; + + const _SlideFadeTransition({ + Key? key, + required this.child, + required this.index, + }) : super(key: key); + + @override + State<_SlideFadeTransition> createState() => _SlideFadeTransitionState(); +} + +class _SlideFadeTransitionState extends State<_SlideFadeTransition> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _opacity; + late final Animation _offset; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + + _opacity = CurvedAnimation(parent: _controller, curve: Curves.easeOut); + _offset = Tween(begin: const Offset(0, 0.1), end: Offset.zero) + .animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); + + Future.delayed(Duration(milliseconds: 50 * (widget.index % 5)), () { + if (mounted) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _opacity, + child: SlideTransition( + position: _offset, + child: widget.child, + ), + ); + } +} diff --git a/lib/views/radar/radar_details/radar_details.dart b/lib/views/radar/radar_details/radar_details.dart index d4c14b5..49e2d9c 100644 --- a/lib/views/radar/radar_details/radar_details.dart +++ b/lib/views/radar/radar_details/radar_details.dart @@ -29,6 +29,7 @@ class _RadarDetailsState extends State { } }); state.args = widget.pageData['args'] ?? const RadarRequestArgs(page: 0); + state.initialDescription = widget.pageData['description']; Future.delayed(Duration.zero, () { state.getRadarDetails(widget.pageData['id']); }); diff --git a/lib/views/radar/radar_details/radar_details_state.dart b/lib/views/radar/radar_details/radar_details_state.dart index ef3e3f8..705d243 100644 --- a/lib/views/radar/radar_details/radar_details_state.dart +++ b/lib/views/radar/radar_details/radar_details_state.dart @@ -15,6 +15,7 @@ class RadarDetailsState extends CoreProvier { int _trackingTimerCounter = 0; late final int initialIndex; late final RadarRequestArgs args; + String? initialDescription; bool isFetchingNewItem = false; final List relatedQueue = []; bool openComments = false; @@ -32,13 +33,11 @@ class RadarDetailsState extends CoreProvier { } Future getRadarDetails(int id, {bool? isForward}) async { - // Prevent duplicate requests if (_isRequestInProgress) { return; } _isRequestInProgress = true; - // Set loading state if (isForward == null) { appState = AppState.busy; } else { @@ -47,7 +46,6 @@ class RadarDetailsState extends CoreProvier { } try { - // Wait for token to be ready if (RequestService.token == null || RequestService.token!.isEmpty) { await Future.delayed(const Duration(milliseconds: 500)); if (RequestService.token == null || RequestService.token!.isEmpty) { @@ -62,6 +60,15 @@ class RadarDetailsState extends CoreProvier { if (service.isSuccess) { final result = service.result; final radar = RadarDetailsData.fromJson(result['radar']); + + if (isForward == null && initialDescription != null) { + radar.description = initialDescription; + } + + if (isForward == null && radar.description == null) { + await _fetchDescriptionFromList(radar.id, radar); + } + if (args.page == 0) { radars.add(radar); initialIndex = 0; @@ -82,7 +89,8 @@ class RadarDetailsState extends CoreProvier { } if (isForward == null) { - radars.addAll(List.generate(max(radar.order - 2, 0), (index) => null)); + radars + .addAll(List.generate(max(radar.order - 2, 0), (index) => null)); if (prevRadar != null) { radars.add(prevRadar); } @@ -127,6 +135,36 @@ class RadarDetailsState extends CoreProvier { } } + Future _fetchDescriptionFromList( + int radarId, RadarDetailsData radarItem) async { + try { + final service = RequestService( + RequestHelper.radarOverviews( + args: const RadarRequestArgs(page: 1), + ), + ); + await service.httpGet(); + + if (service.isSuccess) { + final radarList = service.result['radars'] as List?; + if (radarList != null) { + for (var item in radarList) { + if (item['id'] == radarId) { + final radarData = OverviewData.fromJson(item); + if (radarData.description.isNotEmpty) { + radarItem.description = radarData.description; + } + break; + } + } + } + } + } catch (e) { + // ignore: avoid_print + print('Error fetching description from list: $e'); + } + } + Future getRelatedContents() async { if (currentRadar.relatedContents.isNotEmpty && currentRadar.relatedContentsIsEmpty) { @@ -197,4 +235,4 @@ class RadarDetailsState extends CoreProvier { _trackingTimer.cancel(); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/views/radar/radar_state.dart b/lib/views/radar/radar_state.dart index d30a0e3..a8b7c84 100644 --- a/lib/views/radar/radar_state.dart +++ b/lib/views/radar/radar_state.dart @@ -14,6 +14,8 @@ class RadarState extends CoreProvier { String? endDate; int page = 1; int lastPage = 1; + int visibleCount = 4; + bool isScrolled = false; bool shouldColapse = false; final List selectedCats = []; @@ -37,11 +39,20 @@ class RadarState extends CoreProvier { search = ''; lastSearch = ''; isScrolled = false; + visibleCount = 4; if (!isInit) { getRadars(page: 1); } } + void loadMore() { + visibleCount += 4; + if (visibleCount >= radars.length && page < lastPage) { + getRadars(page: page + 1); + } + notifyListeners(); + } + Future getRadars({ required int page, }) async { diff --git a/lib/views/widgets/ai_chat_dialog.dart b/lib/views/widgets/ai_chat_dialog.dart index 736d16e..acb5960 100644 --- a/lib/views/widgets/ai_chat_dialog.dart +++ b/lib/views/widgets/ai_chat_dialog.dart @@ -1,5 +1,8 @@ +// ignore_for_file: deprecated_member_use + import 'package:didvan/services/ai_rag_service.dart'; import 'package:didvan/services/ai_voice_service.dart'; +import 'package:didvan/services/network/request.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/providers/user.dart'; import 'package:flutter/material.dart'; @@ -8,6 +11,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:provider/provider.dart'; import 'package:record/record.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:just_audio/just_audio.dart'; import 'dart:ui'; import 'dart:io'; @@ -28,7 +32,10 @@ class _AiChatDialogState extends State late AnimationController _animationController; late AnimationController _pulseController; final AudioRecorder _audioRecorder = AudioRecorder(); + final AudioPlayer _audioPlayer = AudioPlayer(); String? _recordingPath; + String? _currentPlayingUrl; + bool _isPlaying = false; @override void initState() { @@ -37,6 +44,18 @@ class _AiChatDialogState extends State vsync: this, duration: const Duration(milliseconds: 400), ); + + _audioPlayer.playerStateStream.listen((state) { + if (mounted) { + setState(() { + _isPlaying = state.playing; + if (state.processingState == ProcessingState.completed) { + _isPlaying = false; + _currentPlayingUrl = null; + } + }); + } + }); _pulseController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), @@ -66,6 +85,7 @@ class _AiChatDialogState extends State _animationController.dispose(); _pulseController.dispose(); _audioRecorder.dispose(); + _audioPlayer.dispose(); super.dispose(); } @@ -81,6 +101,68 @@ class _AiChatDialogState extends State }); } + Future _toggleAudioPlayback(String audioUrl) async { + try { + if (_isPlaying && _currentPlayingUrl == audioUrl) { + await _audioPlayer.pause(); + setState(() { + _isPlaying = false; + }); + } else { + if (_currentPlayingUrl != audioUrl) { + final token = RequestService.token; + debugPrint('🎵 Audio URL: $audioUrl'); + debugPrint('🔑 Token exists: ${token != null && token.isNotEmpty}'); + + AudioSource? audioSource; + + try { + if (token != null && token.isNotEmpty) { + audioSource = AudioSource.uri( + Uri.parse(audioUrl), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + debugPrint('✅ Trying with token (no Bearer prefix)'); + } else { + audioSource = AudioSource.uri( + Uri.parse(audioUrl), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + debugPrint('✅ Trying without Authorization header'); + } + + await _audioPlayer.setAudioSource(audioSource); + setState(() { + _currentPlayingUrl = audioUrl; + }); + } catch (headerError) { + debugPrint( + '⚠️ Error with headers, trying simple URL: $headerError'); + await _audioPlayer.setUrl(audioUrl); + setState(() { + _currentPlayingUrl = audioUrl; + }); + } + } + await _audioPlayer.play(); + setState(() { + _isPlaying = true; + }); + } + } catch (e) { + debugPrint('❌ Error playing audio: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('خطا در پخش صوت')), + ); + } + } + } + Future _sendMessage() async { final message = _messageController.text.trim(); if (message.isEmpty) return; @@ -105,6 +187,7 @@ class _AiChatDialogState extends State isUser: false, timestamp: DateTime.now(), sources: response.sources, + audioUrl: response.audioUrl, )); _isLoading = false; }); @@ -171,6 +254,7 @@ class _AiChatDialogState extends State isUser: false, timestamp: DateTime.now(), sources: ragResponse.sources, + audioUrl: ragResponse.audioUrl, )); _isLoading = false; }); @@ -310,7 +394,6 @@ class _AiChatDialogState extends State ); }, ), - // Avatar Container( width: 44, height: 44, @@ -334,7 +417,6 @@ class _AiChatDialogState extends State ), ), ), - // Online indicator Positioned( bottom: 1, left: 1, @@ -550,6 +632,67 @@ class _AiChatDialogState extends State ), ), ], + if (!message.isUser && + message.audioUrl != null && + message.audioUrl!.isNotEmpty) ...[ + const SizedBox(height: 8), + InkWell( + onTap: () => _toggleAudioPlayback(message.audioUrl!), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF0066AA).withOpacity(0.1), + const Color(0xFF0088DD).withOpacity(0.08), + ], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFF0066AA).withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF0066AA), + Color(0xFF0088DD), + ], + ), + shape: BoxShape.circle, + ), + child: Icon( + (_isPlaying && + _currentPlayingUrl == message.audioUrl) + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + size: 16, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + DidvanText( + (_isPlaying && + _currentPlayingUrl == message.audioUrl) + ? 'در حال پخش...' + : 'پخش صوتی پاسخ', + fontSize: 12, + color: const Color(0xFF0066AA), + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ), + ], ], ), ), @@ -686,7 +829,7 @@ class _AiChatDialogState extends State mainAxisSize: MainAxisSize.min, children: [ Text( - 'در حال تایپ...', + 'در حال فکر کردن...', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, @@ -917,11 +1060,13 @@ class ChatMessage { final bool isUser; final DateTime timestamp; final List sources; + final String? audioUrl; ChatMessage({ required this.text, required this.isUser, required this.timestamp, this.sources = const [], + this.audioUrl, }); } diff --git a/lib/views/widgets/category_item.dart b/lib/views/widgets/category_item.dart index a192a76..c3f7427 100644 --- a/lib/views/widgets/category_item.dart +++ b/lib/views/widgets/category_item.dart @@ -1,5 +1,4 @@ import 'package:didvan/config/design_config.dart'; -import 'package:didvan/config/theme_data.dart'; import 'package:didvan/models/category.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; @@ -26,7 +25,7 @@ class CategoryItem extends StatelessWidget { return ds.height / 16 * 9 / 3; } } - return ds.width / 3; + return ds.width / 2; } bool _useWebMobileLayout(context) { @@ -44,14 +43,14 @@ class CategoryItem extends StatelessWidget { child: AnimatedContainer( duration: DesignConfig.mediumAnimationDuration, padding: isColapsed ? const EdgeInsets.all(4) : EdgeInsets.zero, - width: isColapsed ? 100 : _width(context), + width: isColapsed ? 200 : _width(context), alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: DesignConfig.lowBorderRadius, - border: isColapsed - ? Border.all(color: Theme.of(context).colorScheme.focusedBorder) - : null, - ), + // decoration: BoxDecoration( + // borderRadius: DesignConfig.lowBorderRadius, + // border: isColapsed + // ? Border.all(color: Theme.of(context).colorScheme.focusedBorder) + // : null, + // ), child: Column( children: [ AnimatedVisibility( @@ -64,13 +63,16 @@ class CategoryItem extends StatelessWidget { height: !_useWebMobileLayout(context) ? _width(context) / 2 : ds.width / 7, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: DesignConfig.defaultShadow, - borderRadius: DesignConfig.mediumBorderRadius, - ), + // decoration: BoxDecoration( + // color: Theme.of(context).colorScheme.surface, + // boxShadow: DesignConfig.defaultShadow, + // borderRadius: DesignConfig.mediumBorderRadius, + // ), padding: const EdgeInsets.all(8), - child: SvgPicture.asset(category.asset!), + child: SvgPicture.asset( + category.asset!, + height: 50, + ), ), ), const SizedBox( @@ -78,7 +80,7 @@ class CategoryItem extends StatelessWidget { ), DidvanText( category.label, - color: Theme.of(context).colorScheme.title, + color: const Color.fromARGB(255, 25, 93, 128), style: Theme.of(context).textTheme.titleSmall, ), ], diff --git a/lib/views/widgets/didvan/icon_button.dart b/lib/views/widgets/didvan/icon_button.dart index 774f44a..4064021 100644 --- a/lib/views/widgets/didvan/icon_button.dart +++ b/lib/views/widgets/didvan/icon_button.dart @@ -1,13 +1,16 @@ import 'package:didvan/views/widgets/ink_wrapper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class DidvanIconButton extends StatelessWidget { - final IconData icon; + final dynamic icon; final Color? color; final Color? backgroundColor; final double? size; final double? gestureSize; final VoidCallback onPressed; + final bool isSvg; + const DidvanIconButton({ Key? key, required this.icon, @@ -16,6 +19,7 @@ class DidvanIconButton extends StatelessWidget { this.size, this.gestureSize, this.backgroundColor, + this.isSvg = false, }) : super(key: key); @override @@ -30,12 +34,22 @@ class DidvanIconButton extends StatelessWidget { ), height: gestureSize ?? 48, width: gestureSize ?? 48, - child: Icon( - icon, - size: size, - color: color, + child: Center( + child: isSvg + ? SvgPicture.asset( + icon as String, + width: size, + height: size, + colorFilter: color != null + ? ColorFilter.mode(color!, BlendMode.srcIn) + : null, + ) + : Icon( + icon as IconData, + size: size, + color: color, + ), ), - ), ); } diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index c762cba..8d20b0a 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -1,5 +1,6 @@ // ignore_for_file: deprecated_member_use +import 'dart:async'; import 'dart:math'; import 'package:carousel_slider/carousel_slider.dart'; @@ -9,14 +10,11 @@ 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/models/studio_details_data.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/utils/date_time.dart'; -import 'package:didvan/views/direct/widgets/audio_widget.dart'; -import 'package:didvan/views/widgets/overview/multitype.dart'; -import 'package:didvan/views/widgets/tag_item.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'; @@ -24,9 +22,14 @@ 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 { @@ -54,183 +57,662 @@ class DidvanPageView extends StatefulWidget { } 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) { - final double deviceTopPadding = MediaQuery.of(context).padding.top; - return 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: 0.94, - enableInfiniteScroll: false, + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + 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); + }, + ), + ], + ), + ), ), - itemBuilder: (context, index, realIndex) => Directionality( - textDirection: TextDirection.rtl, - child: SizedBox( - height: MediaQuery.of(context).size.height, - child: SingleChildScrollView( - controller: index == widget.currentIndex - ? widget.scrollController - : null, - physics: const BouncingScrollPhysics(), - padding: EdgeInsets.only( - left: 4, - right: 4, - top: 16 + deviceTopPadding, - bottom: 92, - ), - child: DidvanCard( - padding: EdgeInsets.zero, - enableBorder: false, - child: Builder( - builder: (context) { - final item = widget.items[index]; - if (item == null) { - return const SizedBox(); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SkeletonImage( - imageUrl: item.image, - aspectRatio: 16 / 9, - ), - const SizedBox(height: 20), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16), - child: DidvanText( - item.title, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - const SizedBox(height: 8), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16), - child: _subtitle(item), - ), - if (widget.isRadar && item.podcast != null) - Container( - padding: const EdgeInsets.all(12) - .copyWith(left: 0, bottom: 4), - margin: const EdgeInsets.all(12), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.hint, - ), - borderRadius: DesignConfig.lowBorderRadius, - color: - Theme.of(context).colorScheme.background, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + )), + 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: [ - const DidvanText('این مطلب را بشنوید:'), - const SizedBox(height: 8), - AudioWidget( - id: item.id, - audioUrl: item.podcast, - audioMetaData: StudioDetailsData( - id: item.id, - link: item.podcast, - createdAt: item.createdAt, - order: 0, - marked: item.marked, - comments: 0, - tags: [], - iframe: '', - duration: item.duration, - title: item.title, - image: item.image, - description: 'radar', - type: 'radar', + 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: MultitypeOverview( + item: item.relatedContents[i], + onMarkChanged: (id, value) {}, + ), + ), + const SizedBox(height: 8), + ], + ), ), ), ], ), ), - 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', - ), - ], + ); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + 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: const TextStyle( + fontSize: 12, + color: Color.fromARGB(255, 102, 102, 102), ), ), - const Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 16, + 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); + }, ), - child: DidvanDivider(verticalPadding: 0), ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 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.only(bottom: 8), - child: MultitypeOverview( - item: item.relatedContents[i], - onMarkChanged: (id, value) {}, - ), + const SizedBox(width: 8), + DidvanText( + _formatDuration(position), + style: const TextStyle( + fontSize: 12, + color: Color.fromARGB(255, 102, 102, 102), ), - const SizedBox(height: 8), + ), ], ); }, - ), - ), + ); + }, ), ), - ), + ], ), ), - Positioned( - right: 24, - top: 24 + deviceTopPadding, - child: _BackButton(scrollController: widget.scrollController), + 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: const Color.fromARGB(255, 102, 102, 102), + ), + 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: const Color.fromARGB(255, 102, 102, 102))), + child: DidvanText( + '${snapshot.data!.toString().replaceAll('.0', '')}X', + style: const TextStyle( + fontWeight: FontWeight.w900, + color: Color.fromARGB(255, 102, 102, 102), + 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), ], ); } @@ -324,14 +806,17 @@ class _DidvanPageViewState extends State { 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: [ @@ -385,32 +870,30 @@ class _DidvanPageViewState extends State { return Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ - DidvanText( - 'رادار ', - style: Theme.of(context).textTheme.bodySmall, + SvgPicture.asset('lib/assets/icons/calendar.svg'), + const SizedBox( + width: 5, ), - for (var i = 0; i < item.categories.length; i++) - DidvanText( - item.categories[i].label + - '${i != item.categories.length - 1 ? '،' : ''} ', - style: Theme.of(context).textTheme.bodySmall, - ), DidvanText( - ' - ${DateTimeUtils.momentGenerator(item.createdAt)}', - style: Theme.of(context).textTheme.bodySmall, + DateTime.parse(item.createdAt!).toPersianDateStr(), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: const Color.fromARGB(255, 102, 102, 102), + ), ), ], ); } else { return Row( children: [ - DidvanText( - item.reference, - style: Theme.of(context).textTheme.bodySmall, + SvgPicture.asset('lib/assets/icons/calendar.svg'), + const SizedBox( + width: 5, ), DidvanText( - ' - ${DateTimeUtils.momentGenerator(item.createdAt)}', - style: Theme.of(context).textTheme.bodySmall, + DateTime.parse(item.createdAt!).toPersianDateStr(), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: const Color.fromARGB(255, 102, 102, 102), + ), ), ], ); @@ -420,6 +903,7 @@ class _DidvanPageViewState extends State { class _BackButton extends StatefulWidget { final ScrollController? scrollController; + // ignore: unused_element_parameter const _BackButton({Key? key, this.scrollController}) : super(key: key); @override @@ -488,3 +972,79 @@ class __BackButtonState extends State<_BackButton> { ); } } + +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, + ), + ); + } +} diff --git a/lib/views/widgets/floating_navigation_bar.dart b/lib/views/widgets/floating_navigation_bar.dart index b348212..27f5f29 100644 --- a/lib/views/widgets/floating_navigation_bar.dart +++ b/lib/views/widgets/floating_navigation_bar.dart @@ -101,10 +101,7 @@ class _FloatingNavigationBarState extends State { height: 48, decoration: BoxDecoration( color: Theme.of(context).colorScheme.navigation, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), - ), + borderRadius: BorderRadius.circular(24), border: DesignConfig.isDark ? Border.all( color: Theme.of(context).colorScheme.cardBorder, @@ -138,28 +135,28 @@ class _FloatingNavigationBarState extends State { ), ), const Spacer(), - DidvanIconButton( - gestureSize: 32, - onPressed: () => Navigator.of(context).pushNamed( - Routes.mentions, - arguments: { - 'id': widget.item.id, - 'type': widget.isRadar ? 'radar' : 'news', - 'title': widget.item.title, - }, + SizedBox( + width: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DidvanIconButton( + gestureSize: 32, + onPressed: () => Navigator.of(context).pushNamed( + Routes.mentions, + arguments: { + 'id': widget.item.id, + 'type': widget.isRadar ? 'radar' : 'news', + 'title': widget.item.title, + }, + ), + icon: 'lib/assets/icons/fluent_mention-32-regular.svg', + isSvg: true, + size: 25, + ), + ], ), - icon: DidvanIcons.mention_icon, ), - const Spacer(), - DidvanIconButton( - gestureSize: 32, - onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil( - Routes.home, - (route) => false, - ), - icon: DidvanIcons.house_regular, - ), - const Spacer(), BookmarkButton( itemId: widget.item.id, type: widget.isRadar ? 'radar' : 'news', @@ -174,9 +171,10 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); } }, - gestureSize: 32, + gestureSize: 22, + svgIconOn: 'lib/assets/icons/bookmark_fill.svg', + svgIconOff: 'lib/assets/icons/archive-tick.svg', ), - const Spacer(), SizedBox( width: 60, child: Row( @@ -198,12 +196,13 @@ class _FloatingNavigationBarState extends State { 'onCommentsChanged': widget.onCommentsChanged, }, ), - icon: DidvanIcons.chats_regular, + icon: 'lib/assets/icons/message-text.svg', + isSvg: true, + size: 25, ), ], ), ), - const Spacer(), DidvanIconButton( gestureSize: 32, onPressed: diff --git a/lib/views/widgets/overview/news.dart b/lib/views/widgets/overview/news.dart index 9b64b04..5ed5e27 100644 --- a/lib/views/widgets/overview/news.dart +++ b/lib/views/widgets/overview/news.dart @@ -38,6 +38,7 @@ class NewsOverview extends StatelessWidget { 'id': news.id, 'args': newsRequestArgs, 'hasUnmarkConfirmation': hasUnmarkConfirmation, + 'description': news.description, }, ), child: Column( @@ -79,28 +80,38 @@ class NewsOverview extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - SvgPicture.asset('lib/assets/icons/calendar.svg'), - const SizedBox( - width: 5, - ), - DidvanText( - DateTime.parse(news.createdAt).toPersianDateStr(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox( - width: 20, - ), - SvgPicture.asset('lib/assets/icons/global.svg'), - const SizedBox( - width: 5, - ), - DidvanText( - news.reference!, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + Expanded( + child: Row( + children: [ + SvgPicture.asset('lib/assets/icons/calendar.svg'), + const SizedBox( + width: 5, + ), + Flexible( + child: DidvanText( + DateTime.parse(news.createdAt).toPersianDateStr(), + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox( + width: 20, + ), + SvgPicture.asset('lib/assets/icons/global.svg'), + const SizedBox( + width: 5, + ), + Flexible( + child: DidvanText( + news.reference!, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), Row( children: [ @@ -185,4 +196,4 @@ class NewsOverview extends StatelessWidget { ], ), ); -} +} \ No newline at end of file diff --git a/lib/views/widgets/overview/radar.dart b/lib/views/widgets/overview/radar.dart index df13ec9..f02b89e 100644 --- a/lib/views/widgets/overview/radar.dart +++ b/lib/views/widgets/overview/radar.dart @@ -1,17 +1,16 @@ import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/widgets/bookmark_button.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; -import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/liked_button.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/flutter_svg.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; class RadarOverview extends StatelessWidget { @@ -34,6 +33,8 @@ class RadarOverview extends StatelessWidget { @override Widget build(BuildContext context) { return DidvanCard( + padding: const EdgeInsets.all(0), + enableBorder: true, onTap: () => Navigator.of(context).pushNamed( Routes.radarDetails, arguments: { @@ -42,22 +43,33 @@ class RadarOverview extends StatelessWidget { 'id': radar.id, 'args': radarRequestArgs, 'hasUnmarkConfirmation': hasUnmarkConfirmation, + 'description': radar.description, }, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - DidvanText( - radar.title, - fontWeight: FontWeight.w600, + SkeletonImage( + imageUrl: radar.image, + aspectRatio: 16 / 8, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + bottomLeft: Radius.circular(0), + bottomRight: Radius.circular(0), + )), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: DidvanText( + radar.title, + fontWeight: FontWeight.w600, + ), ), - const SizedBox(height: 8), Stack( children: [ - SkeletonImage( - imageUrl: radar.image, - aspectRatio: 16 / 9, - ), if (radar.forManagers) Positioned( top: 0, @@ -80,73 +92,95 @@ class RadarOverview extends StatelessWidget { ), ], ), - const SizedBox(height: 8), - Row( - children: [ - DidvanText( - radar.categories!.first.label, - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.caption, - ), - const Spacer(), - DidvanText( - '${DateTime.parse(radar.createdAt).toPersianDateStr()} | خواندن در ${radar.timeToRead} دقیقه', - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.caption, - ), - ], + const SizedBox(height: 5), + Padding( + padding: const EdgeInsets.all(8.0), + child: DidvanText( + radar.description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), ), - const SizedBox(height: 8), - DidvanText( - radar.description, - maxLines: 3, - overflow: TextOverflow.ellipsis, + const Padding( + padding: EdgeInsets.only(left: 8, right: 8), + child: DidvanDivider(), ), - const DidvanDivider(), Row( children: [ if (radar.comments != 0) DidvanText(radar.comments.toString()), const SizedBox(width: 4), - DidvanIconButton( - icon: DidvanIcons.chats_regular, - gestureSize: 24, - onPressed: () => Navigator.of(context).pushNamed( - Routes.comments, - arguments: { - 'type': 'radar', - 'title': radar.title, - 'id': radar.id, - 'onCommentsChanged': (count) => - onCommentsChanged(radar.id, count) - }, + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 20), + child: Row( + children: [ + SvgPicture.asset('lib/assets/icons/calendar.svg'), + const SizedBox( + width: 5, + ), + DidvanText( + DateTime.parse(radar.createdAt).toPersianDateStr(), + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(fontWeight: FontWeight.bold), + color: Theme.of(context).colorScheme.caption, + ), + const SizedBox( + width: 15, + ), + SvgPicture.asset( + 'lib/assets/icons/ion_extension-puzzle-outline.svg', + height: 17, + ), + const SizedBox( + width: 5, + ), + DidvanText( + radar.categories!.first.label, + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(fontWeight: FontWeight.bold), + color: Theme.of(context).colorScheme.caption, + ), + ], ), ), const Spacer(), - Row( - children: [ - LikedButton( - itemId: radar.id, - type: 'radar', - gestureSize: 32, - value: radar.liked, - onMarkChanged: (value) => - onLikedChanged(radar.id, value, false), - askForConfirmation: hasUnmarkConfirmation, - likes: radar.likes, - ), - const SizedBox( - width: 4.0, - ), - BookmarkButton( - itemId: radar.id, - type: 'radar', - gestureSize: 32, - value: radar.marked, - onMarkChanged: (value) => - onMarkChanged(radar.id, value, false), - askForConfirmation: hasUnmarkConfirmation, - ), - ], + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 5), + child: Row( + children: [ + LikedButton( + itemId: radar.id, + type: 'radar', + gestureSize: 32, + value: radar.liked, + onMarkChanged: (value) => + onLikedChanged(radar.id, value, false), + askForConfirmation: hasUnmarkConfirmation, + likes: radar.likes, + unlikedColor: const Color.fromARGB(255, 102, 102, 102), + ), + const SizedBox( + width: 4.0, + ), + BookmarkButton( + itemId: radar.id, + type: 'radar', + gestureSize: 22, + value: radar.marked, + onMarkChanged: (value) => + onMarkChanged(radar.id, value, false), + askForConfirmation: hasUnmarkConfirmation, + svgIconOn: 'lib/assets/icons/bookmark_fill.svg', + svgIconOff: 'lib/assets/icons/archive-tick.svg', + color: const Color.fromARGB(255, 102, 102, 102), + unbookmarkedColor: + const Color.fromARGB(255, 102, 102, 102), + ), + ], + ), ), ], ),