From 5bdb516275983d562c300386f839c501af7d2237 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Wed, 18 Jun 2025 16:56:20 +0330 Subject: [PATCH] added story and change houshan --- lib/assets/icons/CONCEPTS.svg | 10 + lib/assets/icons/INDUSTRY_ENVIRONMENT.svg | 10 + lib/assets/icons/MACRO_ENVIRONMENT.svg | 10 + lib/assets/icons/MACRO_TRENDS.svg | 10 + lib/assets/icons/MODERN_INVESTMENTS.svg | 10 + lib/assets/icons/Mic Solid.svg | 4 + lib/assets/icons/houshanNav/aichatS.svg | 3 + lib/assets/icons/houshanNav/aichatU.svg | 10 + lib/assets/icons/houshanNav/houshan.svg | 4 + .../icons/houshanNav/imagegeneratorS.svg | 11 + .../icons/houshanNav/imagegeneratorU.svg | 13 + lib/assets/icons/houshanNav/searchS.svg | 3 + lib/assets/icons/houshanNav/searchU.svg | 4 + lib/assets/icons/houshanNav/translateS.svg | 11 + lib/assets/icons/houshanNav/translateU.svg | 13 + lib/assets/icons/paperclip.svg | 3 + lib/main.dart | 3 + lib/models/story_model.dart | 122 +++++ lib/providers/user.dart | 2 + lib/routes/route_generator.dart | 359 ++++++++----- lib/routes/routes.dart | 5 +- lib/services/ai/ai_api_service.dart | 20 +- lib/services/network/request_helper.dart | 3 + lib/services/story_service.dart | 71 +++ lib/utils/date_time.dart | 12 +- lib/views/ai/ai.dart | 189 +++---- lib/views/ai/ai_chat_page.dart | 196 +++---- lib/views/ai/ai_chat_state.dart | 2 +- lib/views/ai/ai_state.dart | 1 + lib/views/ai/create_bot_assistants_page.dart | 8 + lib/views/ai/tool_screen.dart | 122 +---- lib/views/ai/widgets/ai_message_bar.dart | 28 +- lib/views/ai/widgets/ai_message_bar_ios.dart | 2 +- lib/views/ai/widgets/hoshan_drawer.dart | 11 +- .../ai/widgets/tool_category_view_widget.dart | 178 +++++++ lib/views/ai_section/ai_section_page.dart | 182 +++++++ .../ai_section/widgets/ai_section_bnb.dart | 176 +++++++ .../authentication/authentication_state.dart | 34 +- .../screens/reset_password.dart | 41 +- .../authentication/screens/username.dart | 3 +- lib/views/home/home.dart | 488 +++++++++--------- lib/views/home/home_state.dart | 2 +- lib/views/home/main/main_page.dart | 287 +++++----- lib/views/home/main/main_page_state.dart | 41 +- lib/views/home/main/widgets/banner.dart | 5 +- .../home/main/widgets/story_section.dart | 124 +++++ lib/views/story_viewer/story_viewer_page.dart | 406 +++++++++++++++ lib/views/widgets/ai_banner.dart | 4 +- lib/views/widgets/didvan/bnb.dart | 11 +- lib/views/widgets/didvan/text_field.dart | 8 +- pubspec.yaml | 1 + 51 files changed, 2342 insertions(+), 934 deletions(-) create mode 100644 lib/assets/icons/CONCEPTS.svg create mode 100644 lib/assets/icons/INDUSTRY_ENVIRONMENT.svg create mode 100644 lib/assets/icons/MACRO_ENVIRONMENT.svg create mode 100644 lib/assets/icons/MACRO_TRENDS.svg create mode 100644 lib/assets/icons/MODERN_INVESTMENTS.svg create mode 100644 lib/assets/icons/Mic Solid.svg create mode 100644 lib/assets/icons/houshanNav/aichatS.svg create mode 100644 lib/assets/icons/houshanNav/aichatU.svg create mode 100644 lib/assets/icons/houshanNav/houshan.svg create mode 100644 lib/assets/icons/houshanNav/imagegeneratorS.svg create mode 100644 lib/assets/icons/houshanNav/imagegeneratorU.svg create mode 100644 lib/assets/icons/houshanNav/searchS.svg create mode 100644 lib/assets/icons/houshanNav/searchU.svg create mode 100644 lib/assets/icons/houshanNav/translateS.svg create mode 100644 lib/assets/icons/houshanNav/translateU.svg create mode 100644 lib/assets/icons/paperclip.svg create mode 100644 lib/models/story_model.dart create mode 100644 lib/services/story_service.dart create mode 100644 lib/views/ai/widgets/tool_category_view_widget.dart create mode 100644 lib/views/ai_section/ai_section_page.dart create mode 100644 lib/views/ai_section/widgets/ai_section_bnb.dart create mode 100644 lib/views/home/main/widgets/story_section.dart create mode 100644 lib/views/story_viewer/story_viewer_page.dart diff --git a/lib/assets/icons/CONCEPTS.svg b/lib/assets/icons/CONCEPTS.svg new file mode 100644 index 0000000..5b579ac --- /dev/null +++ b/lib/assets/icons/CONCEPTS.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/INDUSTRY_ENVIRONMENT.svg b/lib/assets/icons/INDUSTRY_ENVIRONMENT.svg new file mode 100644 index 0000000..d84f675 --- /dev/null +++ b/lib/assets/icons/INDUSTRY_ENVIRONMENT.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/MACRO_ENVIRONMENT.svg b/lib/assets/icons/MACRO_ENVIRONMENT.svg new file mode 100644 index 0000000..89b28c0 --- /dev/null +++ b/lib/assets/icons/MACRO_ENVIRONMENT.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/MACRO_TRENDS.svg b/lib/assets/icons/MACRO_TRENDS.svg new file mode 100644 index 0000000..f63cb1d --- /dev/null +++ b/lib/assets/icons/MACRO_TRENDS.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/MODERN_INVESTMENTS.svg b/lib/assets/icons/MODERN_INVESTMENTS.svg new file mode 100644 index 0000000..153817f --- /dev/null +++ b/lib/assets/icons/MODERN_INVESTMENTS.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/Mic Solid.svg b/lib/assets/icons/Mic Solid.svg new file mode 100644 index 0000000..5cbfced --- /dev/null +++ b/lib/assets/icons/Mic Solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/houshanNav/aichatS.svg b/lib/assets/icons/houshanNav/aichatS.svg new file mode 100644 index 0000000..9c63017 --- /dev/null +++ b/lib/assets/icons/houshanNav/aichatS.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/houshanNav/aichatU.svg b/lib/assets/icons/houshanNav/aichatU.svg new file mode 100644 index 0000000..674f6ec --- /dev/null +++ b/lib/assets/icons/houshanNav/aichatU.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/houshanNav/houshan.svg b/lib/assets/icons/houshanNav/houshan.svg new file mode 100644 index 0000000..03645a8 --- /dev/null +++ b/lib/assets/icons/houshanNav/houshan.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/houshanNav/imagegeneratorS.svg b/lib/assets/icons/houshanNav/imagegeneratorS.svg new file mode 100644 index 0000000..d752854 --- /dev/null +++ b/lib/assets/icons/houshanNav/imagegeneratorS.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/assets/icons/houshanNav/imagegeneratorU.svg b/lib/assets/icons/houshanNav/imagegeneratorU.svg new file mode 100644 index 0000000..a894dff --- /dev/null +++ b/lib/assets/icons/houshanNav/imagegeneratorU.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/lib/assets/icons/houshanNav/searchS.svg b/lib/assets/icons/houshanNav/searchS.svg new file mode 100644 index 0000000..6136990 --- /dev/null +++ b/lib/assets/icons/houshanNav/searchS.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/houshanNav/searchU.svg b/lib/assets/icons/houshanNav/searchU.svg new file mode 100644 index 0000000..bd214df --- /dev/null +++ b/lib/assets/icons/houshanNav/searchU.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/houshanNav/translateS.svg b/lib/assets/icons/houshanNav/translateS.svg new file mode 100644 index 0000000..f066e22 --- /dev/null +++ b/lib/assets/icons/houshanNav/translateS.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/assets/icons/houshanNav/translateU.svg b/lib/assets/icons/houshanNav/translateU.svg new file mode 100644 index 0000000..ef4d8ca --- /dev/null +++ b/lib/assets/icons/houshanNav/translateU.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/lib/assets/icons/paperclip.svg b/lib/assets/icons/paperclip.svg new file mode 100644 index 0000000..6a900fc --- /dev/null +++ b/lib/assets/icons/paperclip.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/main.dart b/lib/main.dart index e820f1a..0d6b045 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -191,6 +191,9 @@ class _DidvanState extends State with WidgetsBindingObserver { ChangeNotifierProvider( create: (context) => BotAssistantsState(), ), + // ChangeNotifierProvider( + // create: (context) => StoryViewerState(), + // ), ], child: Consumer( builder: (context, themeProvider, child) => Container( diff --git a/lib/models/story_model.dart b/lib/models/story_model.dart new file mode 100644 index 0000000..22d9b98 --- /dev/null +++ b/lib/models/story_model.dart @@ -0,0 +1,122 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:flutter/material.dart'; + +enum MediaType { image, video, gif } + +// Represents a single story item (image, gif, or video) +class StoryItem { + final int id; + final String url; + final MediaType media; + final Duration duration; + final ValueNotifier isViewed; + + StoryItem({ + required this.id, + required this.url, + required this.media, + required this.duration, + bool viewed = false, + }) : isViewed = ValueNotifier(viewed); + + factory StoryItem.fromJson(Map json) { + MediaType mediaType; + switch (json['mediaType']) { + case 'VIDEO': + mediaType = MediaType.video; + break; + case 'GIF': + mediaType = MediaType.gif; + break; + case 'IMAGE': + default: + mediaType = MediaType.image; + break; + } + + return StoryItem( + id: json['id'], + url: json['mediaUrl'], + media: mediaType, + // API doesn't provide duration for images/gifs, so we use a default. + // For videos, the player controller will determine the duration. + duration: const Duration(seconds: 10), + ); + } +} + +class User { + final String name; + final String profileImageUrl; + final String createdAt; + + const User({ + required this.name, + required this.profileImageUrl, + required this.createdAt, + }); +} + +// This class will wrap the API response and fit it into the UI's expected structure. +class UserStories { + final int id; // The main story ID from the API + final User user; + final List stories; + + UserStories({ + required this.id, + required this.user, + required this.stories, + }); + + factory UserStories.fromApiJson(Map json) { + final List items = json['items'] ?? []; + final List completedIds = json['completedStoryItemIds'] ?? []; + + return UserStories( + id: json['id'], + user: User( + name: json['title'], // Using title directly from the API + profileImageUrl: + _mapCategoryToIcon(json['category']), // Mapping category to an icon + createdAt: json['createdAt'], + ), + stories: items.map((item) { + final storyItem = StoryItem.fromJson(item); + // Check if this story item has been viewed. + if (completedIds.contains(storyItem.id)) { + storyItem.isViewed.value = true; + } + return storyItem; + }).toList(), + ); + } +} + + +String _mapCategoryToIcon(String category) { + switch (category) { + case 'MACRO_TRENDS': + return 'lib/assets/icons/MACRO_TRENDS.svg'; + case 'INDUSTRY_ENVIRONMENT': + return 'lib/assets/icons/INDUSTRY_ENVIRONMENT.svg'; + case 'COMPETITION_ENVIRONMENT': + return Assets.startup; + case 'OPPORTUNITIES': + return Assets.hugeideas; + case 'THREATS': + return Assets.risk; + case 'TECHNOLOGY': + return Assets.tech; + case 'STARTUPS': + return Assets.startup; + case 'CONCEPTS': + return 'lib/assets/icons/CONCEPTS.svg'; + case 'MACRO_ENVIRONMENT': + return 'lib/assets/icons/MACRO_ENVIRONMENT.svg'; + case 'MODERN_INVESTMENTS': + return 'lib/assets/icons/MODERN_INVESTMENTS.svg'; + default: + return Assets.stats; + } +} \ No newline at end of file diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 77fd775..464412a 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -67,6 +67,8 @@ class UserProvider extends CoreProvier { await _registerFirebaseToken(); + notifyListeners(); + return true; } catch (e) { return false; diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 01394e7..7ab73a4 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -1,13 +1,12 @@ -// ignore_for_file: unused_import, deprecated_member_use - import 'package:didvan/models/ai/ai_chat_args.dart'; +import 'package:didvan/models/story_model.dart'; import 'package:didvan/views/ai/ai_chat_page.dart'; import 'package:didvan/views/ai/ai_chat_state.dart'; +import 'package:didvan/views/ai_section/ai_section_page.dart'; import 'package:didvan/views/ai/bot_assistants_page.dart'; import 'package:didvan/views/ai/create_bot_assistants_page.dart'; import 'package:didvan/views/ai/create_bot_assistants_state.dart'; import 'package:didvan/views/ai/history_ai_chat_page.dart'; -import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/ai/info_page.dart'; import 'package:didvan/views/ai/info_state.dart'; import 'package:didvan/views/authentication/authentication.dart'; @@ -25,7 +24,6 @@ import 'package:didvan/views/home/infography/infography_screen.dart'; import 'package:didvan/views/home/infography/infography_screen_state.dart'; import 'package:didvan/views/home/main/main_page_state.dart'; import 'package:didvan/views/home/home_state.dart'; -import 'package:didvan/views/home/new_statistic/new_statistic.dart'; import 'package:didvan/views/home/new_statistic/new_statistics_state.dart'; import 'package:didvan/views/home/new_statistic/statistics_details/stat_cats_general_screen.dart'; import 'package:didvan/views/home/new_statistic/statistics_details/stat_cats_general_state.dart'; @@ -61,6 +59,8 @@ import 'package:didvan/views/podcasts/studio_details/studio_details.mobile.dart' if (dart.library.html) 'package:didvan/views/podcasts/studio_details/studio_details.web.dart'; import 'package:didvan/views/splash/splash.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/story_viewer/story_viewer_page.dart'; + import 'package:didvan/views/webview/web_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -80,45 +80,68 @@ class RouteGenerator { return _createRoute( const Splash(), ); - case Routes.notificationTime: + case Routes.aiSection: return _createRoute( - ChangeNotifierProvider( - create: (context) => NotificationTimeState(), - child: NotificationTime( - pageData: settings.arguments as Map, - )), + const AiSectionPage(), ); + case Routes.notificationTime: + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => NotificationTimeState(), + child: NotificationTime( + pageData: settings.arguments as Map, + )), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); case Routes.favouritesStep: - return _createRoute( - ChangeNotifierProvider( - create: (context) => CustomizeCategoryState(), - child: FavoritesStep( - pageData: settings.arguments as Map, - )), - ); - - // case Routes.newStatic: - // return _createRoute(ChangeNotifierProvider( - // create: (context) => NewStatisticState(), - // child: const NewStatistic())); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => CustomizeCategoryState(), + child: FavoritesStep( + pageData: settings.arguments as Map, + )), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); case Routes.notificationStatusStep: - return _createRoute( - ChangeNotifierProvider( - create: (context) => CustomizeCategoryState(), - child: NotificationStatusStep( - pageData: settings.arguments as Map, - )), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => CustomizeCategoryState(), + child: NotificationStatusStep( + pageData: settings.arguments as Map, + )), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); case Routes.authenticaion: + bool isResetPassword = false; + if (settings.arguments is bool) { + isResetPassword = settings.arguments as bool; + } else if (settings.arguments != null) { + print('Warning: Invalid argument type for isResetPassword. Expected bool, got ${settings.arguments.runtimeType}. Using default false.'); + } return _createRoute( ChangeNotifierProvider( create: (context) => AuthenticationState(), - child: Authentication(isResetPassword: settings.arguments as bool), + child: Authentication(isResetPassword: isResetPassword), ), ); case Routes.home: + bool? showDialogsArg; + if (settings.arguments is bool?) { + showDialogsArg = settings.arguments as bool?; + } else if (settings.arguments != null) { + print('Warning: Invalid argument type for showDialogs. Expected bool?, got ${settings.arguments.runtimeType}. Using default null.'); + } return _createRoute( MultiProvider( providers: [ @@ -138,7 +161,7 @@ class RouteGenerator { create: (context) => NewStatisticState()) ], child: Home( - showDialogs: settings.arguments as bool?, + showDialogs: showDialogsArg, ), ), ); @@ -195,51 +218,76 @@ class RouteGenerator { )); case Routes.radarDetails: - return _createRoute( - ChangeNotifierProvider( - create: (context) => RadarDetailsState(), - child: RadarDetails( - pageData: settings.arguments as Map, - ), - ), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => RadarDetailsState(), + child: RadarDetails( + pageData: settings.arguments as Map, + ), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.statGeneral: - return _createRoute(MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (context) => StatGeneralScreenState()), - ChangeNotifierProvider( - create: (context) => NewStatisticState()), - ], - child: StatGeneralScreen( - pageData: settings.arguments as Map), - )); + if (settings.arguments is Map) { + return _createRoute(MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => StatGeneralScreenState()), + ChangeNotifierProvider( + create: (context) => NewStatisticState()), + ], + child: StatGeneralScreen( + pageData: settings.arguments as Map), + )); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.newsDetails: - return _createRoute( - ChangeNotifierProvider( - create: (context) => NewsDetailsState(), - child: NewsDetails( - pageData: settings.arguments as Map, - ), - ), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => NewsDetailsState(), + child: NewsDetails( + pageData: settings.arguments as Map, + ), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.studioDetails: - return _createRoute( - StudioDetails( - pageData: settings.arguments as Map, - ), - ); + if (settings.arguments is Map) { + return _createRoute( + StudioDetails( + pageData: settings.arguments as Map, + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.statisticDetails: - return _createRoute( - ChangeNotifierProvider( - create: (context) => StatisticDetailsState(), - child: StatisticDetails( - pageData: settings.arguments as Map, - ), - ), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => StatisticDetailsState(), + child: StatisticDetails( + pageData: settings.arguments as Map, + ), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.stock: return _createRoute(MultiProvider( @@ -259,30 +307,46 @@ class RouteGenerator { ), ); case Routes.direct: - return _createRoute( - ChangeNotifierProvider( - create: (context) => DirectState(), - child: Direct(pageData: settings.arguments as Map), - ), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => DirectState(), + child: + Direct(pageData: settings.arguments as Map), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.comments: - return _createRoute( - ChangeNotifierProvider( - create: (context) => CommentsState(), - child: Comments( - pageData: settings.arguments as Map, - ), - ), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => CommentsState(), + child: Comments( + pageData: settings.arguments as Map, + ), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.mentions: - return _createRoute( - ChangeNotifierProvider( - create: (context) => MentionsState(), - child: Mentions( - pageData: settings.arguments as Map, - ), - ), - ); + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => MentionsState(), + child: Mentions( + pageData: settings.arguments as Map, + ), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); + case Routes.bookmarks: return _createRoute( ChangeNotifierProvider( @@ -291,39 +355,58 @@ class RouteGenerator { ), ); case Routes.hashtag: - return _createRoute( - ChangeNotifierProvider( - create: (context) => HashtagState(), - child: - Hashtag(pageData: settings.arguments as Map), - ), - ); - case Routes.filteredBookmarks: - final args = settings.arguments as Map; - final type = args['type'] as int; - final onDeleted = args['onDeleted'] as void Function(int id)?; + if (settings.arguments is Map) { + return _createRoute( + ChangeNotifierProvider( + create: (context) => HashtagState(), + child: Hashtag( + pageData: settings.arguments as Map), + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map.'); - return _createRoute( - ChangeNotifierProvider( - create: (context) => FilteredBookmarksState( - type, - ), - child: FilteredBookmarks( - onDeleted: onDeleted, - type: type, - ), - ), - ); + + case Routes.filteredBookmarks: + if (settings.arguments is Map) { + final args = settings.arguments as Map; + if (args['type'] is int) { + final type = args['type'] as int; + final onDeleted = args['onDeleted'] as void Function(int id)?; + return _createRoute( + ChangeNotifierProvider( + create: (context) => FilteredBookmarksState(type), + child: FilteredBookmarks( + onDeleted: onDeleted, + type: type, + ), + ), + ); + } else { + return _errorRoute( + 'Invalid arguments for ${settings.name}: "type" key is missing or not an int.'); + } + } else { + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected a Map.'); + } case Routes.aiChat: - return _createRoute(ChangeNotifierProvider( - create: (context) => AiChatState(), - child: AiChatPage( - args: settings.arguments as AiChatArgs, - ))); + if (settings.arguments is AiChatArgs) { + return _createRoute(ChangeNotifierProvider( + create: (context) => AiChatState(), + child: AiChatPage( + args: settings.arguments as AiChatArgs, + ))); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected AiChatArgs.'); + + case Routes.aiHistory: return _createRoute(HistoryAiChatPage( - archived: settings.arguments as bool?, + archived: settings.arguments as bool?, )); case Routes.botAssistants: @@ -354,22 +437,42 @@ class RouteGenerator { child: const InfoPage())); case Routes.web: - return _createRoute(WebView( - src: settings.arguments as String, - )); + if (settings.arguments is String) { + return _createRoute(WebView( + src: settings.arguments as String, + )); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected String.'); + + case Routes.storyViewer: + if (settings.arguments is Map) { + final args = settings.arguments as Map; + final stories = args['stories'] as List; + final tappedIndex = args['tappedIndex'] as int; + return _createRoute( + StoryViewerPage( + stories: stories, + tappedIndex: tappedIndex, + ), + ); + } + return _errorRoute( + 'Invalid arguments for ${settings.name}: Expected Map with stories and tappedIndex.'); + default: - return _errorRoute(settings.name ?? ''); + return _errorRoute(settings.name ?? 'Unknown route'); } } - static Route _errorRoute(String name) { + static Route _errorRoute(String message) { return MaterialPageRoute(builder: (_) { return Scaffold( appBar: AppBar( - title: const Text('Error'), + title: const Text('خطا در مسیریابی'), ), body: Center( - child: Text('$name is not valid'), + child: Text('مسیر "$message" معتبر نیست یا آرگومان‌های نادرستی ارسال شده است.'), ), ); }); @@ -411,4 +514,4 @@ class RouteGenerator { }, ); } -} +} \ No newline at end of file diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 12bb84b..872d5af 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -7,6 +7,8 @@ class Routes { static const String info = '/info-page'; static const String home = '/home'; + static const String aiSection = '/ai-section'; + static const String radars = '/radars'; static const String news = '/news'; static const String infography = '/infography'; @@ -38,4 +40,5 @@ class Routes { static const String widgetSetting = '/widget-setting'; static const String newStatic = '/new-static'; static const String web = '/web'; -} + static const String storyViewer = '/story-viewer'; +} \ No newline at end of file diff --git a/lib/services/ai/ai_api_service.dart b/lib/services/ai/ai_api_service.dart index 695a301..a1631f4 100644 --- a/lib/services/ai/ai_api_service.dart +++ b/lib/services/ai/ai_api_service.dart @@ -68,7 +68,6 @@ class AiApiService { // bytes = reader.result as Uint8List; } } else { - // For other platforms bytes = await file.main.readAsBytes(); } if (file.isRecorded) { @@ -98,20 +97,19 @@ class AiApiService { Future>> getResponse(http.MultipartRequest req) async { try { final response = await http.Client().send(req); - if (response.statusCode == 400) { - // Handle 400 response - final errorResponse = await response.stream.bytesToString(); - final errorJson = jsonDecode(errorResponse); - throw Exception(errorJson['error'] ?? 'Bad Request'); - } else if (response.statusCode != 200) { - // Handle other non-200 responses - throw Exception('Failed to load data'); + if (response.statusCode != 200) { + final errorBody = await response.stream.bytesToString(); + print( + 'body: ${req.url} sc: ${response.statusCode}'); + print('res: $errorBody'); + throw Exception( + 'sc is ${response.statusCode}'); } else { return response.stream.asBroadcastStream(); } } catch (e) { - // Handle any other errors - throw Exception('Failed to load data'); + print('req ${req.url}: $e'); + throw Exception('Failed to load data due to an exception.'); } } } diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index fbac640..6f9ffda 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -7,6 +7,8 @@ import 'package:didvan/models/requests/studio.dart'; class RequestHelper { static const String baseUrl = 'https://api.didvan.app'; static const String baseUrl2 = 'http://opportunity-threat.didvan.com'; + static const String storiesV1 = '$baseUrl2/api/v1/stories'; + static const String storyActivity = '$baseUrl2/api/v1/stories/activity'; static const String _baseUserUrl = '$baseUrl/user'; static const String _baseRadarUrl = '$baseUrl/radar'; static const String _baseNewsUrl = '$baseUrl/news'; @@ -18,6 +20,7 @@ class RequestHelper { static const String _baseHomeUrl = '$baseUrl/home'; static const String _baseNewStats = '$baseUrl/home/statistic'; static const String _baseNewStatsSearch = '$baseUrl/home/statistic/search'; + static const String checkHasPassword = '$_baseUserUrl/checkHasPassword'; static const String mainPageContent = _baseHomeUrl; static String searchAll({ diff --git a/lib/services/story_service.dart b/lib/services/story_service.dart new file mode 100644 index 0000000..0f70a9e --- /dev/null +++ b/lib/services/story_service.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'package:didvan/main.dart'; +import 'package:didvan/models/story_model.dart'; +import 'package:didvan/providers/user.dart'; +import 'package:didvan/services/network/request.dart'; +import 'package:didvan/services/network/request_helper.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; + +class StoryService { + static Future> getStories() async { + // دریافت UserProvider از طریق navigatorKey + final userProvider = + Provider.of(navigatorKey.currentContext!, listen: false); + final userId = userProvider.user.id; + final token = RequestService.token; + + // اضافه کردن userId به عنوان پارامتر به URL + final uri = Uri.parse(RequestHelper.storiesV1).replace(queryParameters: { + 'userId': userId.toString(), + }); + + final response = await http.get( + uri, // استفاده از uri جدید + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + // ... بقیه کد بدون تغییر باقی می‌ماند + if (response.statusCode == 200) { + final decodedBody = utf8.decode(response.bodyBytes); + final dynamic jsonData = json.decode(decodedBody); + + List storyList; + + if (jsonData is List) { + storyList = jsonData; + } else if (jsonData is Map && + jsonData.containsKey('content')) { + storyList = jsonData['content']; + } else { + throw Exception('Unexpected JSON structure for stories response'); + } + + return storyList.map((json) => UserStories.fromApiJson(json)).toList(); + } else { + throw Exception( + 'Failed to load stories with status code: ${response.statusCode}'); + } + } + + static Future markStoryAsViewed(int storyId, int storyItemId) async { + final userProvider = + Provider.of(navigatorKey.currentContext!, listen: false); + final userId = userProvider.user.id; + + final service = RequestService( + RequestHelper.storyActivity, + body: { + 'userId': userId, + 'storyId': storyId, + 'storyItemId': storyItemId, + }, + ); + + await service.post(); + return service.isSuccess; + } +} \ No newline at end of file diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart index d6e3387..57cb924 100644 --- a/lib/utils/date_time.dart +++ b/lib/utils/date_time.dart @@ -159,26 +159,26 @@ class DateTimeUtils { double interval = seconds / 31536000; if (interval > 1) { - return "${interval.floor()} سال پیش"; + return "منتشر شده در ${interval.floor()} سال پیش"; } interval = seconds / 2592000; if (interval > 1) { - return "${interval.floor()} ماه پیش"; + return "منتشر شده در ${interval.floor()} ماه پیش"; } interval = seconds / 86400; if (interval > 1) { if (interval.floor() == 1) return 'دیروز'; - return "${interval.floor()} روز پیش"; + return "منتشر شده در ${interval.floor()} روز پیش"; } interval = seconds / 3600; if (interval > 1) { - return "${interval.floor()} ساعت پیش"; + return "منتشر شده در ${interval.floor()} ساعت پیش"; } interval = seconds / 60; if (interval > 1) { - return "${interval.floor()} دقیقه پیش"; + return "منتشر شده در ${interval.floor()} دقیقه پیش"; } return 'هم اکنون'; // return seconds.floor().toString() + " ثانیه پیش"; } -} +} \ No newline at end of file diff --git a/lib/views/ai/ai.dart b/lib/views/ai/ai.dart index f0c6a7d..c8dd299 100644 --- a/lib/views/ai/ai.dart +++ b/lib/views/ai/ai.dart @@ -1,5 +1,3 @@ -// ignore_for_file: library_private_types_in_public_api - import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; @@ -10,22 +8,20 @@ import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/views/ai/ai_state.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/ai/tool_screen.dart'; - import 'package:didvan/views/ai/widgets/message_bar_btn.dart'; -import 'package:didvan/views/home/home.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/state_handlers/empty_connection.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; class Ai extends StatefulWidget { const Ai({Key? key}) : super(key: key); @override + // ignore: library_private_types_in_public_api _AiState createState() => _AiState(); } @@ -100,81 +96,7 @@ class _AiState extends State { ); } - return GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1 / 1, - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8), - itemCount: tools.length, - shrinkWrap: true, - padding: const EdgeInsets.all(16), - physics: - const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - final tool = tools[index]; - return InkWell( - onTap: () => context - .read() - .goToToolBox(tool: tool), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: DesignConfig - .lowBorderRadius, - color: Theme.of(context) - .colorScheme - .surface, - border: Border.all( - color: Theme.of(context) - .colorScheme - .border, - ), - ), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SvgPicture.network( - tool.image!, - width: 48, - height: 48, - ), - const SizedBox( - width: 8, - ), - DidvanText( - tool.name ?? '', - style: Theme.of(context) - .textTheme - .titleSmall, - maxLines: 2, - overflow: - TextOverflow.ellipsis, - ), - Flexible( - child: DidvanText( - tool.description ?? '', - style: Theme.of(context) - .textTheme - .bodySmall, - overflow: - TextOverflow.ellipsis, - color: Theme.of(context) - .colorScheme - .caption, - maxLines: 2, - textAlign: - TextAlign.center, - ), - ) - ], - ), - ), - ); - }, - ); + return const SizedBox(); }, ) ], @@ -189,45 +111,55 @@ class _AiState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ InkWell( - onTap: () => ActionSheetUtils(context) - .botsDialogSelect(context: context), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - DidvanIcons.caret_down_solid, - color: - Theme.of(context).colorScheme.title, - size: 16, - ), - const SizedBox( - width: 12, - ), - DidvanText( - bot.name.toString(), - color: - Theme.of(context).colorScheme.title, - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - const SizedBox( - width: 12, - ), - SkeletonImage( - width: 28, - height: 28, - imageUrl: bot.image.toString(), - borderRadius: - BorderRadius.circular(360), - ), - ], - ), - ), + onTap: () { + final historyState = + context.read(); + if (historyState.bots.isEmpty) { + historyState.getBots(); + } + ActionSheetUtils(context) + .botsDialogSelect(context: context); + }, + borderRadius: BorderRadius.circular(12), + child: const SizedBox(), + // Padding( + // padding: const EdgeInsets.symmetric( + // vertical: 8.0, horizontal: 12.0), + // child: Row( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: + // CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Icon(DidvanIcons.angle_down_light, + // size: 16, + // color: Theme.of(context) + // .colorScheme + // .title), + // const SizedBox( + // width: 8, + // ), + // DidvanText( + // bot.name.toString(), + // color: Theme.of(context) + // .colorScheme + // .title, + // style: const TextStyle( + // fontWeight: FontWeight.bold), + // ), + // const SizedBox( + // width: 12, + // ), + // SkeletonImage( + // width: 28, + // height: 28, + // imageUrl: bot.image.toString(), + // borderRadius: + // BorderRadius.circular(360), + // ), + // ], + // ), + // ), ), InkWell( onTap: () => Navigator.of(context) @@ -242,17 +174,12 @@ class _AiState extends State { Expanded( child: Container( decoration: BoxDecoration( - boxShadow: DesignConfig - .defaultShadow, color: Theme.of(context) .colorScheme .surface, border: Border.all( - color: Theme.of(context) - .colorScheme - .border), - borderRadius: DesignConfig - .highBorderRadius), + color: const Color.fromARGB(255, 0, 126, 167),width: 1.5), + borderRadius: BorderRadius.circular(50)), child: Row( children: [ Expanded( @@ -291,7 +218,7 @@ class _AiState extends State { InputBorder .none, hintText: - 'بنویسید...', + 'بنویسید یا پیام صوتی بگذارید...', hintStyle: Theme.of( context) .textTheme @@ -318,7 +245,7 @@ class _AiState extends State { }, enable: false, icon: Icons - .attach_file_rounded), + .add), ], )))) ], @@ -332,7 +259,7 @@ class _AiState extends State { EdgeInsets.fromLTRB(8, 8, 8, 4), child: FittedBox( child: DidvanText( - 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.', + 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.',style: TextStyle(fontWeight: FontWeight.w600), ), ), ) @@ -354,7 +281,7 @@ class _AiState extends State { top: 32, right: 0, child: InkWell( - onTap: () => homeScaffKey.currentState!.openDrawer(), + onTap: () => Scaffold.of(context).openDrawer(), child: Container( width: 46, height: 46, @@ -445,4 +372,4 @@ class _AiState extends State { }, ); } -} +} \ No newline at end of file diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart index 5171316..a294bed 100644 --- a/lib/views/ai/ai_chat_page.dart +++ b/lib/views/ai/ai_chat_page.dart @@ -59,7 +59,6 @@ class _AiChatPageState extends State { state.chatId = widget.args.chat!.id!; state.chat = widget.args.chat; } - // JsInteropService().showAlert(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (state.chatId != null) { state.getAllMessages(state.chatId!).then((value) => Future.delayed( @@ -429,103 +428,104 @@ class _AiChatPageState extends State { .toString() .contains('user') && widget.args.assistantsName == null) - PopupMenuButton( - offset: const Offset(0, 46), - onSelected: (value) async { - navigatorKey.currentState!.pushNamed( - Routes.aiChat, - arguments: AiChatArgs( - bot: value, - prompts: message, - isTool: widget.args.isTool ?? - context - .read< - HistoryAiChatState>() - .bots)); - }, - itemBuilder: (BuildContext context) { - final bots = widget.args.isTool ?? - context - .read() - .bots; - return [ - ...List.generate( - bots.length, - (index) => PopupMenuItem( - value: bots[index], - height: 72, - child: Container( - constraints: - const BoxConstraints( - maxWidth: 200), - child: Row( - children: [ - SkeletonImage( - imageUrl: bots[index] - .image - .toString(), - width: 42, - height: 42, - borderRadius: - BorderRadius - .circular(360), - ), - const SizedBox(width: 12), - Expanded( - child: Directionality( - textDirection: - TextDirection.ltr, - child: DidvanText( - bots[index] - .name - .toString(), - maxLines: 1, - overflow: - TextOverflow - .ellipsis, - ), - ), - ), - ], - ), - ), - ), - ) - ]; - }, - child: Container( - alignment: Alignment.center, - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.symmetric( - horizontal: 8), - constraints: const BoxConstraints( - maxWidth: 100), - decoration: BoxDecoration( - borderRadius: - DesignConfig.lowBorderRadius, - border: Border.all( - color: Theme.of(context) - .colorScheme - .title)), - child: Row( - children: [ - Expanded( - child: Directionality( - textDirection: - TextDirection.ltr, - child: DidvanText( - '${widget.args.assistantsName ?? widget.args.bot.name}', - maxLines: 1, - overflow: - TextOverflow.ellipsis, - ), - ), - ), - const Icon( - DidvanIcons.angle_down_light), - ], - ), - )), + // PopupMenuButton( + // offset: const Offset(0, 46), + // onSelected: (value) async { + // navigatorKey.currentState!.pushNamed( + // Routes.aiChat, + // arguments: AiChatArgs( + // bot: value, + // prompts: message, + // isTool: widget.args.isTool ?? + // context + // .read< + // HistoryAiChatState>() + // .bots)); + // }, + // itemBuilder: (BuildContext context) { + // final bots = widget.args.isTool ?? + // context + // .read() + // .bots; + // return [ + // ...List.generate( + // bots.length, + // (index) => PopupMenuItem( + // value: bots[index], + // height: 72, + // child: Container( + // constraints: + // const BoxConstraints( + // maxWidth: 200), + // child: Row( + // children: [ + // SkeletonImage( + // imageUrl: bots[index] + // .image + // .toString(), + // width: 42, + // height: 42, + // borderRadius: + // BorderRadius + // .circular(360), + // ), + // const SizedBox(width: 12), + // Expanded( + // child: Directionality( + // textDirection: + // TextDirection.ltr, + // child: DidvanText( + // bots[index] + // .name + // .toString(), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ), + // ), + // ], + // ), + // ), + // ), + // ) + // ]; + // }, + // // child: Container( + // // alignment: Alignment.center, + // // margin: const EdgeInsets.all(8), + // // padding: const EdgeInsets.symmetric( + // // horizontal: 8), + // // constraints: const BoxConstraints( + // // maxWidth: 100), + // // decoration: BoxDecoration( + // // borderRadius: + // // DesignConfig.lowBorderRadius, + // // border: Border.all( + // // color: Theme.of(context) + // // .colorScheme + // // .title)), + // // child: Row( + // // children: [ + // // Expanded( + // // child: Directionality( + // // textDirection: + // // TextDirection.ltr, + // // child: DidvanText( + // // '${widget.args.assistantsName ?? widget.args.bot.name}', + // // maxLines: 1, + // // overflow: + // // TextOverflow.ellipsis, + // // ), + // // ), + // // ), + // // const Icon( + // // DidvanIcons.angle_down_light), + // // ], + // // ), + // // ) + // ), if (message.role .toString() .contains('user') && diff --git a/lib/views/ai/ai_chat_state.dart b/lib/views/ai/ai_chat_state.dart index a131527..aed21fc 100644 --- a/lib/views/ai/ai_chat_state.dart +++ b/lib/views/ai/ai_chat_state.dart @@ -57,6 +57,7 @@ class AiChatState extends CoreProvier { await ActionSheetUtils(navigatorKey.currentContext!).showAlert(AlertData( message: 'خطا در برقراری ارتباط', aLertType: ALertType.error)); update(); + print("error is ${e.toString()}"); } Future getChatId() async { @@ -195,7 +196,6 @@ class AiChatState extends CoreProvier { if (kIsWeb) { try { int startIndex = responseMessgae.indexOf('{{{'); -// + 3 to include the }}} characters String slicedText = responseMessgae.substring(startIndex, responseMessgae.length); diff --git a/lib/views/ai/ai_state.dart b/lib/views/ai/ai_state.dart index ea6a029..5bc2a02 100644 --- a/lib/views/ai/ai_state.dart +++ b/lib/views/ai/ai_state.dart @@ -10,6 +10,7 @@ class AiState extends CoreProvier { bool loading = true; List? tools; + void getTools() async { final service = RequestService( diff --git a/lib/views/ai/create_bot_assistants_page.dart b/lib/views/ai/create_bot_assistants_page.dart index c9ede74..b0bdd62 100644 --- a/lib/views/ai/create_bot_assistants_page.dart +++ b/lib/views/ai/create_bot_assistants_page.dart @@ -61,6 +61,14 @@ class _CreateBotAssistantsPageState extends State { @override void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final state = context.read(); + if (widget.id != null) { + state.getAnAssistant(id: widget.id!); + } else { + state.getImageToolsBots(); + } + }); } void onConfirm(CreateBotAssistantsState state) async { diff --git a/lib/views/ai/tool_screen.dart b/lib/views/ai/tool_screen.dart index 95c547e..09c08bf 100644 --- a/lib/views/ai/tool_screen.dart +++ b/lib/views/ai/tool_screen.dart @@ -1,123 +1,19 @@ -import 'package:didvan/config/design_config.dart'; -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/models/ai/ai_chat_args.dart'; import 'package:didvan/models/ai/tools_model.dart'; -import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/ai/ai_state.dart'; -import 'package:didvan/views/widgets/didvan/text.dart'; -import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:didvan/views/ai/widgets/tool_category_view_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; -class ToolScreen extends StatefulWidget { +class ToolScreen extends StatelessWidget { const ToolScreen({Key? key}) : super(key: key); - @override - State createState() => _ToolScreenState(); -} - -class _ToolScreenState extends State { - late Tools tool = context.read().tool!; - - get itemBuilder => null; - @override Widget build(BuildContext context) { - return SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - children: [ - const SizedBox(height: 32), - SvgPicture.network( - tool.image!, - width: 64, - height: 64, - ), - const SizedBox( - height: 4, - ), - DidvanText( - tool.name!, - fontSize: 20, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.checkFav, - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: DidvanText( - tool.guide!, - fontSize: 12, - color: Theme.of(context).colorScheme.caption, - textAlign: TextAlign.justify, - ), - ), - const SizedBox(height: 24), - GridView.builder( - shrinkWrap: true, - itemCount: tool.bots!.length, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 32), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 1 / 1, - crossAxisSpacing: 18, - mainAxisSpacing: 18), - itemBuilder: (context, index) { - final bot = tool.bots![index]; - return InkWell( - onTap: () => Navigator.of(context).pushNamed(Routes.aiChat, - arguments: AiChatArgs(bot: bot, isTool: tool.bots)), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: DesignConfig.lowBorderRadius, - border: Border.all(color: const Color(0xffbbbbbb))), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SkeletonImage( - imageUrl: bot.image!, - width: 72, - height: 72, - borderRadius: BorderRadius.circular(360), - ), - const SizedBox( - height: 4, - ), - DidvanText( - bot.name ?? '', - fontSize: 16, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (bot.short != null) - Column( - children: [ - const SizedBox( - height: 4, - ), - DidvanText( - bot.short!, - fontSize: 12, - fontWeight: FontWeight.bold, - color: const Color(0xffA8A6AC), - // maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ) - ], - ), - ), - ); - }, - ) - ], - ), - ); + final Tools? tool = context.watch().tool; + + if (tool == null) { + return const Center(child: Text("ابزاری انتخاب نشده است.")); + } + return ToolCategoryViewWidget(tool: tool); } -} +} \ No newline at end of file diff --git a/lib/views/ai/widgets/ai_message_bar.dart b/lib/views/ai/widgets/ai_message_bar.dart index d8cd8b4..09c391e 100644 --- a/lib/views/ai/widgets/ai_message_bar.dart +++ b/lib/views/ai/widgets/ai_message_bar.dart @@ -245,11 +245,13 @@ class _AiMessageBarState extends State { children: [ Container( decoration: BoxDecoration( - boxShadow: DesignConfig.defaultShadow, - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: Theme.of(context).colorScheme.border), - borderRadius: DesignConfig.highBorderRadius), + boxShadow: DesignConfig.defaultShadow, + color: Theme.of(context).colorScheme.surface, + border: Border.all( + color: const Color.fromARGB(255, 0, 126, 167), + width: 1.5), + borderRadius: BorderRadius.circular(50), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -293,11 +295,12 @@ class _AiMessageBarState extends State { ], ), MediaQuery.of(context).viewInsets.bottom == 0 - ? const Padding( - padding: EdgeInsets.fromLTRB(8, 8, 8, 4), + ? const Padding( + padding: EdgeInsets.fromLTRB(3, 8, 3, 4), child: DidvanText( 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.', - fontSize: 12, + fontSize: 11, + fontWeight: FontWeight.bold, ), ) : const SizedBox( @@ -406,7 +409,7 @@ class _AiMessageBarState extends State { enable: false, icon: openAttach || state.file != null ? DidvanIcons.close_regular - : Icons.attach_file_outlined, + : Icons.add, click: () { if (_mPlayer!.isPlaying) { stopPlayer(); @@ -557,11 +560,10 @@ class _AiMessageBarState extends State { if (kIsWeb) { Uint8List? bytes = result - .files.first.bytes; // Access the bytes property + .files.first.bytes; - // Store bytes and file name directly in your state or model state.file = FilesModel( - '', // No need for a file path on web + '', name: name, bytes: bytes, audio: false, @@ -663,7 +665,7 @@ class _AiMessageBarState extends State { final blobUrl = html.Url.createObjectUrlFromBlob(blob); state.file = FilesModel( - blobUrl, // No need for a file path on web + blobUrl, name: name, bytes: bytes, audio: true, diff --git a/lib/views/ai/widgets/ai_message_bar_ios.dart b/lib/views/ai/widgets/ai_message_bar_ios.dart index bd7729a..1086e88 100644 --- a/lib/views/ai/widgets/ai_message_bar_ios.dart +++ b/lib/views/ai/widgets/ai_message_bar_ios.dart @@ -552,7 +552,7 @@ class _AiMessageBarIOSState extends State { }, enable: false, icon: Icons - .attach_file_rounded, + .add, ) : const SizedBox(), ), diff --git a/lib/views/ai/widgets/hoshan_drawer.dart b/lib/views/ai/widgets/hoshan_drawer.dart index 2f13fab..e141bd6 100644 --- a/lib/views/ai/widgets/hoshan_drawer.dart +++ b/lib/views/ai/widgets/hoshan_drawer.dart @@ -28,10 +28,15 @@ class HoshanDrawer extends StatefulWidget { class _HoshanDrawerState extends State { @override - initState() { + void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final historyState = context.read(); + if (historyState.chats.isEmpty && !historyState.loadinggetAll) { + historyState.getChats(archived: false); + } + }); } - @override Widget build(BuildContext context) { return Drawer( @@ -150,7 +155,6 @@ class _HoshanDrawerState extends State { state: state, centerEmptyState: false, emptyState: const EmptyList(), - // enableEmptyState: state.chats.isEmpty, placeholder: chatRowPlaceholder(), placeholderCount: 10, builder: (context, state, index) { @@ -286,7 +290,6 @@ class _HoshanDrawerState extends State { color: enable ? Theme.of(context).colorScheme.title : Theme.of(context).colorScheme.disabledText), - // if (!enable) Text('در حال توسعه ...') ], ) ], diff --git a/lib/views/ai/widgets/tool_category_view_widget.dart b/lib/views/ai/widgets/tool_category_view_widget.dart new file mode 100644 index 0000000..1d7e867 --- /dev/null +++ b/lib/views/ai/widgets/tool_category_view_widget.dart @@ -0,0 +1,178 @@ +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/ai/ai_chat_args.dart'; +import 'package:didvan/models/ai/tools_model.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ToolCategoryViewWidget extends StatelessWidget { + final Tools tool; + const ToolCategoryViewWidget({Key? key, required this.tool}) : super(key: key); + + @override + Widget build(BuildContext context) { + const Color customBackgroundColor = Color.fromRGBO(0, 126, 167, 1.0); + + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(bottom: 24), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.only(top: 32, bottom: 16), + child: Column( + children: [ + if (tool.image != null) + SvgPicture.network( + tool.image!, + width: 64, + height: 64, + placeholderBuilder: (BuildContext context) => const SizedBox( + width: 64, + height: 64, + child: Center(child: CircularProgressIndicator(strokeWidth: 2.0)), + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 16), + child: DidvanText( + tool.name ?? "ابزار هوش مصنوعی", + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.checkFav, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + if (tool.guide != null && tool.guide!.isNotEmpty) ...[ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: DidvanText( + tool.guide!, + fontSize: 12, + color: Theme.of(context).colorScheme.caption, + textAlign: TextAlign.justify, + ), + ), + ], + const SizedBox(height: 24), + if (tool.bots != null && tool.bots!.isNotEmpty) + GridView.builder( + shrinkWrap: true, + itemCount: tool.bots!.length, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 32), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1 / 1.1, + crossAxisSpacing: 18, + mainAxisSpacing: 18), + itemBuilder: (context, index) { + final bot = tool.bots![index]; + return InkWell( + onTap: () { + Navigator.of(context).pushNamed(Routes.aiChat, + arguments: AiChatArgs(bot: bot, isTool: tool.bots)); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: DesignConfig.highBorderRadius.topLeft, + topRight: DesignConfig.highBorderRadius.topRight, + bottomRight: DesignConfig.highBorderRadius.bottomRight, + bottomLeft: DesignConfig.highBorderRadius.bottomLeft, + ), + color: Theme.of(context).colorScheme.surface, + border: Border.all(color: Theme.of(context).colorScheme.border)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + height: 20, + decoration: BoxDecoration( + color: customBackgroundColor, + borderRadius: BorderRadius.only( + topLeft: DesignConfig.highBorderRadius.topLeft, + topRight: DesignConfig.highBorderRadius.topRight, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (bot.image != null) + SkeletonImage( + imageUrl: bot.image!, + width: 70, + height: 70, + borderRadius: BorderRadius.circular(360), + ) + else + Icon(DidvanIcons.ai_solid, size: 50, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 15), + Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: customBackgroundColor, + ), + + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: DidvanText( + bot.name ?? '', + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ), + if (bot.short != null && bot.short!.isNotEmpty) ...[ + const SizedBox(height: 8), + Expanded( + child: DidvanText( + bot.short!, + fontSize: 10, + color: Theme.of(context).colorScheme.caption, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ] else const Spacer(), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ) + else + Padding( + padding: const EdgeInsets.all(32.0), + child: Center(child: DidvanText("هیچ رباتی برای ابزار '${tool.name ?? "انتخابی"}' موجود نیست.")), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/ai_section/ai_section_page.dart b/lib/views/ai_section/ai_section_page.dart new file mode 100644 index 0000000..4342152 --- /dev/null +++ b/lib/views/ai_section/ai_section_page.dart @@ -0,0 +1,182 @@ +import 'package:didvan/models/ai/tools_model.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/views/ai/ai.dart'; +import 'package:didvan/views/ai/widgets/hoshan_drawer.dart'; +import 'package:didvan/views/ai_section/widgets/ai_section_bnb.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/hoshan_app_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:didvan/views/ai/history_ai_chat_state.dart'; +import 'package:didvan/views/ai/ai_state.dart'; +import 'package:didvan/views/ai/widgets/tool_category_view_widget.dart'; + +class ImageGenerationScreen extends StatelessWidget { + const ImageGenerationScreen({super.key}); + + @override + Widget build(BuildContext context) { + final aiState = context.watch(); + + if (aiState.tools == null || aiState.tools!.isEmpty) { + if (aiState.loading || aiState.appState == AppState.busy) { + return const Center(child: CircularProgressIndicator()); + } + return const Center( + child: DidvanText('لیست ابزارها هنوز بارگذاری نشده یا خالی است.')); + } + final Tools imageGenToolCategory = aiState.tools![0]; + return ToolCategoryViewWidget(tool: imageGenToolCategory); + } +} + +class VoiceChatScreen extends StatelessWidget { + const VoiceChatScreen({super.key}); + @override + Widget build(BuildContext context) { + final aiState = context.watch(); + + if (aiState.tools == null || aiState.tools!.isEmpty) { + if (aiState.loading || aiState.appState == AppState.busy) { + return const Center(child: CircularProgressIndicator()); + } + return const Center( + child: DidvanText('لیست ابزارها هنوز بارگذاری نشده یا خالی است.')); + } + final Tools voiceChatToolCategory = aiState.tools!.last; + + return ToolCategoryViewWidget(tool: voiceChatToolCategory); + } +} + +class ChartAnalysisScreen extends StatelessWidget { + const ChartAnalysisScreen({super.key}); + @override + Widget build(BuildContext context) { + final aiState = context.watch(); + + if (aiState.tools == null || aiState.tools!.isEmpty) { + if (aiState.loading || aiState.appState == AppState.busy) { + return const Center(child: CircularProgressIndicator()); + } + return const Center( + child: DidvanText('لیست ابزارها هنوز بارگذاری نشده یا خالی است.')); + } + + if (aiState.tools!.length < 2) { + return const Center( + child: + DidvanText('ابزار کافی برای "تحلیل نمودار" وجود ندارد.')); + } + final Tools chartAnalysisToolCategory = aiState.tools![1]; + + return ToolCategoryViewWidget(tool: chartAnalysisToolCategory); + } +} + +class TranslationScreen extends StatelessWidget { + const TranslationScreen({super.key}); + @override + Widget build(BuildContext context) { + final aiState = context.watch(); + + if (aiState.tools == null || aiState.tools!.isEmpty) { + if (aiState.loading || aiState.appState == AppState.busy) { + return const Center(child: CircularProgressIndicator()); + } + return const Center( + child: DidvanText('لیست ابزارها هنوز بارگذاری نشده یا خالی است.')); + } + + if (aiState.tools!.length < 3) { + return const Center( + child: DidvanText('ابزار کافی برای "ترجمه" وجود ندارد.')); + } + final Tools translationToolCategory = aiState.tools![2]; + + return ToolCategoryViewWidget(tool: translationToolCategory); + } +} + +class AiSectionPage extends StatefulWidget { + const AiSectionPage({super.key}); + + @override + State createState() => _AiSectionPageState(); +} + +class _AiSectionPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + int _currentTabIndex = 2; + final GlobalKey _aiSectionScaffoldKey = + GlobalKey(); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 5, vsync: this, initialIndex: 2); + _tabController.addListener(() { + if (mounted) { + setState(() { + _currentTabIndex = _tabController.index; + }); + } + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final historyAiChatState = context.read(); + if (historyAiChatState.bots.isEmpty) { + historyAiChatState.getBots(); + } + + final aiState = context.read(); + aiState.page = 0; + if (aiState.tools == null) { + aiState.getTools(); + } + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _aiSectionScaffoldKey, + appBar: HoshanAppBar( + onBack: () { + final aiState = context.read(); + if (aiState.page != 0) { + aiState.goToAi(); + } else { + Navigator.of(context).pop(); + } + }, + withActions: true, + ), + drawer: HoshanDrawer(scaffKey: _aiSectionScaffoldKey), + body: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + ImageGenerationScreen(), + VoiceChatScreen(), + Ai(), + ChartAnalysisScreen(), + TranslationScreen(), + ], + ), + bottomNavigationBar: AiSectionBNB( + currentTabIndex: _currentTabIndex, + onTabChanged: (index) { + _tabController.animateTo(index); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/ai_section/widgets/ai_section_bnb.dart b/lib/views/ai_section/widgets/ai_section_bnb.dart new file mode 100644 index 0000000..8fde39f --- /dev/null +++ b/lib/views/ai_section/widgets/ai_section_bnb.dart @@ -0,0 +1,176 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/ai/ai_chat_args.dart'; +import 'package:didvan/models/ai/tools_model.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/webview.dart'; +import 'package:didvan/views/ai/ai_state.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class AiSectionBNB extends StatelessWidget { + final int currentTabIndex; + final void Function(int index) onTabChanged; + + const AiSectionBNB( + {Key? key, required this.currentTabIndex, required this.onTabChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + const String cameraSolid = 'lib/assets/icons/houshanNav/imagegeneratorS.svg'; + const String cameraRegular = 'lib/assets/icons/houshanNav/imagegeneratorU.svg'; + const String micSolid = 'lib/assets/icons/houshanNav/aichatS.svg'; + const String micRegular = 'lib/assets/icons/houshanNav/aichatU.svg'; + const String aiSolid = 'lib/assets/icons/houshanNav/houshan.svg'; + const String aiRegular = 'lib/assets/icons/houshanNav/houshan.svg'; + const String searchSolid = 'lib/assets/icons/houshanNav/searchS.svg'; + const String searchRegular = 'lib/assets/icons/houshanNav/searchU.svg'; + const String translateSolid = 'lib/assets/icons/houshanNav/translateS.svg'; + const String translateRegular = 'lib/assets/icons/houshanNav/translateU.svg'; + + return Container( + height: 72, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: const Color(0XFF1B3C59).withValues(alpha: 0.15), + blurRadius: 8, + spreadRadius: 0, + offset: const Offset(0, -8), + ) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _AiNavBarItem( + isSelected: currentTabIndex == 0, + title: 'عکس ساز', + selectedIcon: cameraSolid, + unselectedIcon: cameraRegular, + onTap: () => onTabChanged(0), + ), + _AiNavBarItem( + isSelected: currentTabIndex == 1, + title: 'چت صوتی', + selectedIcon: micSolid, + unselectedIcon: micRegular, + onTap: () { + NativeWebViewLauncher.openWebView( + 'https://www.aisada.ir/app/page1.html'); + }, + ), + _AiNavBarItem( + isSelected: currentTabIndex == 2, + title: '', + selectedIcon: aiSolid, + unselectedIcon: aiRegular, + onTap: () => onTabChanged(2), + ), + _AiNavBarItem( + isSelected: currentTabIndex == 3, + title: 'جست و جو', + selectedIcon: searchSolid, + unselectedIcon: searchRegular, + onTap: () => onTabChanged(3), + ), + _AiNavBarItem( + isSelected: currentTabIndex == 4, + title: 'ترجمه', + selectedIcon: translateSolid, + unselectedIcon: translateRegular, + onTap: () { + final aiState = context.read(); + if (aiState.tools != null && aiState.tools!.length > 2) { + final Tools translationToolCategory = aiState.tools![2]; + if (translationToolCategory.bots != null && translationToolCategory.bots!.isNotEmpty) { + final gptTranslatorBot = translationToolCategory.bots!.first; + Navigator.of(context).pushNamed( + Routes.aiChat, + arguments: AiChatArgs(bot: gptTranslatorBot, isTool: translationToolCategory.bots), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('ابزار ترجمه در حال بارگذاری است...')), + ); + } + }, + ), + ], + ), + ); + } +} + + +class _AiNavBarItem extends StatelessWidget { + final VoidCallback onTap; + final bool isSelected; + final String title; + final String selectedIcon; + final String unselectedIcon; + + const _AiNavBarItem({ + Key? key, + required this.isSelected, + required this.title, + required this.selectedIcon, + required this.unselectedIcon, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + + final double iconSize = title.isEmpty ? 50.0 : 32.0; + + return Expanded( + child: Tooltip( + message: title, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.title, + borderRadius: DesignConfig.highBorderRadius, + boxShadow: DesignConfig.defaultShadow, + ), + child: GestureDetector( + onTap: onTap, + child: Container( + color: Colors.transparent, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + padding: const EdgeInsets.all(4), + duration: DesignConfig.lowAnimationDuration, + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + isSelected ? selectedIcon : unselectedIcon, + width: iconSize, + height: iconSize, + ), + ), + if (title.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: DidvanText( + title, + style: Theme.of(context).textTheme.bodySmall, + color: Theme.of(context).colorScheme.title, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/authentication/authentication_state.dart b/lib/views/authentication/authentication_state.dart index 5a483ba..013740b 100644 --- a/lib/views/authentication/authentication_state.dart +++ b/lib/views/authentication/authentication_state.dart @@ -24,22 +24,30 @@ class AuthenticationState extends CoreProvier { int get currentPageIndex => _currentPageIndex; Future confirmUsername() async { - appState = AppState.isolatedBusy; - final RequestService service = RequestService( - RequestHelper.confirmUsername, - useAutherization: false, - body: {'username': username}, - ); - await service.post(); - if (service.isSuccess && service.result['confirmed']) { - appState = AppState.idle; - currentPageIndex++; + appState = AppState.isolatedBusy; + final RequestService service = RequestService( + RequestHelper.checkHasPassword, + useAutherization: false, + body: {'username': username}, + ); + await service.post(); + + if (service.isSuccess) { + appState = AppState.idle; + + final bool hasPassword = service.result['hasPassword']; + + if (hasPassword) { + currentPageIndex = 1; } else { - appState = AppState.failed; - ActionSheetUtils(navigatorKey.currentContext!) - .showAlert(AlertData(message: service.errorMessage)); + currentPageIndex = 2; } + } else { + appState = AppState.failed; + ActionSheetUtils(navigatorKey.currentContext!) + .showAlert(AlertData(message: service.errorMessage)); } +} Future login(UserProvider userProvider) async { appState = AppState.isolatedBusy; diff --git a/lib/views/authentication/screens/reset_password.dart b/lib/views/authentication/screens/reset_password.dart index 2e34f7f..134ef53 100644 --- a/lib/views/authentication/screens/reset_password.dart +++ b/lib/views/authentication/screens/reset_password.dart @@ -1,3 +1,4 @@ +import 'package:didvan/providers/server_data.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; @@ -49,25 +50,29 @@ class _ResetPasswordState extends State { const Spacer(), DidvanButton( onPressed: () async { - if (!_formKey.currentState!.validate()) return; - final result = await context - .read() - .resetPassword(_password); - if (!mounted) return; - Future.delayed( - Duration.zero, - () { - if (result && context.read().isAuthenticated) { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - return; - } - Navigator.of(context).pushNamed( - Routes.authenticaion, - arguments: false, + if (!_formKey.currentState!.validate()) { + return; + } + + final authState = context.read(); + final userProvider = context.read(); + + final bool resetSuccess = + await authState.resetPassword(_password); + + if (resetSuccess && mounted) { + authState.password = _password; + final String? token = await authState.login(userProvider); + + if (token != null && mounted) { + await ServerDataProvider.getData(); + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.home, + (_) => false, + arguments: true, ); - }, - ); + } + } }, title: 'تغییر رمز عبور', ), diff --git a/lib/views/authentication/screens/username.dart b/lib/views/authentication/screens/username.dart index 9058371..3f21d04 100644 --- a/lib/views/authentication/screens/username.dart +++ b/lib/views/authentication/screens/username.dart @@ -1,3 +1,4 @@ +import 'package:didvan/utils/extension.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; import 'package:didvan/views/authentication/widgets/authentication_layout.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; @@ -44,7 +45,7 @@ class _UsernameInputState extends State { return null; }, onChanged: (value) { - state.username = value; + state.username = value.convertToEnglishNumber(); }, ), ), diff --git a/lib/views/home/home.dart b/lib/views/home/home.dart index 586764a..baebb6c 100644 --- a/lib/views/home/home.dart +++ b/lib/views/home/home.dart @@ -1,264 +1,276 @@ - // ignore_for_file: deprecated_member_use +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/main.dart'; +import 'package:didvan/models/notification_message.dart'; +import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/providers/theme.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:didvan/services/app_initalizer.dart'; +import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/ai/ai_state.dart'; +import 'package:didvan/views/ai/history_ai_chat_state.dart'; +import 'package:didvan/views/ai/widgets/hoshan_drawer.dart'; +import 'package:didvan/views/home/categories/categories_page.dart'; +import 'package:didvan/views/home/main/main_page.dart'; +import 'package:didvan/views/home/home_state.dart'; +import 'package:didvan/views/home/new_statistic/new_statistic.dart'; +import 'package:didvan/views/home/search/search.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/hoshan_app_bar.dart'; +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:didvan/views/widgets/logo_app_bar.dart'; +import 'package:didvan/views/widgets/didvan/bnb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; +import '../../services/app_home_widget/home_widget_repository.dart'; - import 'package:didvan/config/design_config.dart'; - import 'package:didvan/config/theme_data.dart'; - import 'package:didvan/constants/app_icons.dart'; - import 'package:didvan/main.dart'; - import 'package:didvan/models/notification_message.dart'; - import 'package:didvan/models/view/action_sheet_data.dart'; - import 'package:didvan/providers/theme.dart'; - import 'package:didvan/routes/routes.dart'; - import 'package:didvan/services/app_initalizer.dart'; - import 'package:didvan/utils/action_sheet.dart'; - import 'package:didvan/views/ai/ai.dart'; - import 'package:didvan/views/ai/ai_state.dart'; - import 'package:didvan/views/ai/history_ai_chat_state.dart'; - import 'package:didvan/views/ai/widgets/hoshan_drawer.dart'; - import 'package:didvan/views/home/categories/categories_page.dart'; - import 'package:didvan/views/home/main/main_page.dart'; - import 'package:didvan/views/home/home_state.dart'; - import 'package:didvan/views/home/new_statistic/new_statistic.dart'; - import 'package:didvan/views/home/search/search.dart'; - import 'package:didvan/views/widgets/didvan/text.dart'; - import 'package:didvan/views/widgets/hoshan_app_bar.dart'; - import 'package:didvan/views/widgets/ink_wrapper.dart'; - import 'package:didvan/views/widgets/logo_app_bar.dart'; - import 'package:didvan/views/widgets/didvan/bnb.dart'; - import 'package:flutter/foundation.dart'; - import 'package:flutter/material.dart'; - import 'package:flutter/services.dart'; - import 'package:provider/provider.dart'; - import '../../services/app_home_widget/home_widget_repository.dart'; +final GlobalKey homeScaffKey = GlobalKey(); - final GlobalKey homeScaffKey = GlobalKey(); +class Home extends StatefulWidget { + final bool? showDialogs; + const Home({Key? key, this.showDialogs}) : super(key: key); - class Home extends StatefulWidget { - final bool? showDialogs; - const Home({Key? key, this.showDialogs}) : super(key: key); + @override + State createState() => _HomeState(); +} - @override - State createState() => _HomeState(); +class _HomeState extends State + with SingleTickerProviderStateMixin, WidgetsBindingObserver { + late final TabController _tabController; + + Future _showDialog(BuildContext context) async { + WidgetsBinding.instance.addPostFrameCallback((_) { + ActionSheetUtils(context) + .openDialog( + data: ActionSheetData( + hasDismissButton: false, + hasConfirmButton: false, + withoutButtonMode: true, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWrapper( + onPressed: () => Navigator.of(context).pop(), + child: const Icon(DidvanIcons.close_solid, size: 24), + ), + ], + ), + const SizedBox(height: 8), + + SvgPicture.asset(Assets.horizontalLogoWithText, height: 80), + const SizedBox(height: 24), + + DidvanText( + 'به دیدوان، چشم همیشه باز مدیران خوش آمدید', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 24), + ], + ), + ), + ) + .then((value) => ActionSheetUtils(context).openDialog( + data: ActionSheetData( + backgroundColor: Theme.of(context).colorScheme.background, + isBackgroundDropBlur: true, + content: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWrapper( + onPressed: () { + Future.delayed( + Duration.zero, + () => Navigator.of(context).pop(), + ); + }, + child: const Icon( + DidvanIcons.close_solid, + size: 24, + ), + ), + DidvanText( + 'شخصی سازی محتوا', + style: Theme.of(context).textTheme.displaySmall, + color: Theme.of(context).colorScheme.text, + ), + const InkWrapper( + child: Icon( + DidvanIcons.close_regular, + size: 24, + color: Colors.transparent, + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + const DidvanText( + "کاربر گرامی\nلطفا جهت شخصی‌سازی و استفاده بهتر از برنامه، دسته‌بندی‌های مورد علاقه خود و زمان دریافت اعلانات را انتخاب نمایید.") + ], + ), + onConfirmed: () { + Future.delayed( + Duration.zero, + () => + Navigator.of(navigatorKey.currentContext!).pushNamed( + Routes.favouritesStep, + arguments: {"toTimer": true}, + ), + ); + }, + confrimTitle: 'تایید', + ), + )); + }); } - class _HomeState extends State - with SingleTickerProviderStateMixin, WidgetsBindingObserver { - late final TabController _tabController; - - Future _showDialog(BuildContext context) async { - WidgetsBinding.instance.addPostFrameCallback((_) { - ActionSheetUtils(context) - .openDialog( - data: ActionSheetData( - content: const DidvanText( - 'خوش آمدید!\nبرای امنیت بیشتر، رمز عبور خود را تغییر دهید.', - ), - onConfirmed: () { - Future.delayed( - Duration.zero, - () => Navigator.of(context) - .pushNamed(Routes.authenticaion, arguments: true), - ); - }, - isBackgroundDropBlur: false, - confrimTitle: 'تغییر رمز عبور', - dismissTitle: 'بعدا', - ), - ) - .then((value) => ActionSheetUtils(context).openDialog( - data: ActionSheetData( - backgroundColor: Theme.of(context).colorScheme.background, - isBackgroundDropBlur: true, - content: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - InkWrapper( - onPressed: () { - Future.delayed( - Duration.zero, - () => Navigator.of(context).pop(), - ); - }, - child: const Icon( - DidvanIcons.close_solid, - size: 24, - ), - ), - DidvanText( - 'شخصی سازی محتوا', - style: Theme.of(context).textTheme.displaySmall, - color: Theme.of(context).colorScheme.text, - ), - const InkWrapper( - child: Icon( - DidvanIcons.close_regular, - size: 24, - color: Colors.transparent, - ), - ), - ], - ), - const SizedBox( - height: 12, - ), - const DidvanText( - "کاربر گرامی\nلطفا جهت شخصی‌سازی و استفاده بهتر از برنامه، دسته‌بندی‌های مورد علاقه خود و زمان دریافت اعلانات را انتخاب نمایید.") - ], - ), - // hasDismissButton: false, - onConfirmed: () { - Future.delayed( - Duration.zero, - () => - Navigator.of(navigatorKey.currentContext!).pushNamed( - Routes.favouritesStep, - arguments: {"toTimer": true}, - ), - ); - }, - confrimTitle: 'تایید', - ), - )); - }); + @override + void initState() { + if (widget.showDialogs ?? false) { + _showDialog(context); } + // if (!kIsWeb) { + // NotificationService.startListeningNotificationEvents(); + // } - @override - void initState() { - if (widget.showDialogs ?? false) { - _showDialog(context); + final state = context.read(); + DesignConfig.updateSystemUiOverlayStyle(); + _tabController = TabController(length: 4, vsync: this, initialIndex: 0); + state.tabController = _tabController; + _tabController.addListener(() { + state.currentPageIndex = _tabController.index; + if (_tabController.index == 2) { + final state = context.read(); + + state.getBots(); } - // if (!kIsWeb) { - // NotificationService.startListeningNotificationEvents(); - // } - - final state = context.read(); - DesignConfig.updateSystemUiOverlayStyle(); - _tabController = TabController(length: 4, vsync: this, initialIndex: 0); - state.tabController = _tabController; - _tabController.addListener(() { - state.currentPageIndex = _tabController.index; - if (_tabController.index == 2) { - final state = context.read(); - - state.getBots(); + }); + if (!kIsWeb) { + Future.delayed(Duration.zero, () { + HomeWidgetRepository.fetchWidget(); + HomeWidgetRepository.decideWhereToGo(); + NotificationMessage? data = HomeWidgetRepository.data; + if (data != null) { + HomeWidgetRepository.decideWhereToGoNotif(); } + AppInitializer.handleCLick(state, _tabController); }); - if (!kIsWeb) { - Future.delayed(Duration.zero, () { - HomeWidgetRepository.fetchWidget(); - HomeWidgetRepository.decideWhereToGo(); - NotificationMessage? data = HomeWidgetRepository.data; - if (data != null) { - HomeWidgetRepository.decideWhereToGoNotif(); - } - AppInitializer.handleCLick(state, _tabController); - }); - } + } + state.refresh(); + context.read().addListener(() { state.refresh(); - context.read().addListener(() { - state.refresh(); - }); + }); - super.initState(); + super.initState(); + } + + PreferredSizeWidget getAppBar() { + PreferredSizeWidget result = const LogoAppBar(); + if (context.watch().tabController.index == 2) { + result = HoshanAppBar( + onBack: () { + final state = context.read(); + if (state.page == 1) { + state.goToAi(); + } + }, + ); } - PreferredSizeWidget getAppBar() { - PreferredSizeWidget result = const LogoAppBar(); - if (context.watch().tabController.index == 2) { - result = HoshanAppBar( - onBack: () { - final state = context.read(); - if (state.page == 1) { - state.goToAi(); + return result; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: homeScaffKey, + appBar: getAppBar(), + resizeToAvoidBottomInset: false, + drawer: context.watch().tabController.index == 2 + ? HoshanDrawer( + scaffKey: homeScaffKey, + ) + : null, + body: WillPopScope( + onWillPop: () async { + if (context.read().tabController.index == 0) { + if (kIsWeb) { + return true; } - }, - ); - } - - return result; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: homeScaffKey, - appBar: getAppBar(), - resizeToAvoidBottomInset: false, - drawer: context.watch().tabController.index == 2 - ? HoshanDrawer( - scaffKey: homeScaffKey, - ) - : null, - body: WillPopScope( - onWillPop: () async { - if (context.read().tabController.index == 0) { - if (kIsWeb) { - return true; - } - ActionSheetUtils(context).openDialog( - data: ActionSheetData( - content: const DidvanText( - 'آیا قصد خروج از برنامه را دارید؟', - ), - onConfirmed: () { - SystemChannels.platform.invokeMethod('SystemNavigator.pop'); - }, - isBackgroundDropBlur: true, - confrimTitle: 'بله', - dismissTitle: 'خیر', - )); - } else if (context.read().tabController.index == 2) { - switch (context.read().page) { - case 1: - context.read().goToAi(); - break; - - default: - _tabController.animateTo(0); - } - } else { - _tabController.animateTo(0); - } - return false; - }, - child: Consumer( - builder: (context, state, child) => AnimatedCrossFade( - duration: DesignConfig.lowAnimationDuration, - crossFadeState: state.filtering - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: SizedBox( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: _tabController, - children: const [ - MainPage(), - CategoriesPage(), - Ai(), - NewStatistic(), - //Statistic(), - // Bookmarks(), - ], - ), + ActionSheetUtils(context).openDialog( + data: ActionSheetData( + content: const DidvanText( + 'آیا قصد خروج از برنامه را دارید؟', + ), + onConfirmed: () { + SystemChannels.platform.invokeMethod('SystemNavigator.pop'); + }, + isBackgroundDropBlur: true, + confrimTitle: 'بله', + dismissTitle: 'خیر', + )); + } else if (context.read().tabController.index == 2) { + switch (context.read().page) { + case 1: + context.read().goToAi(); + break; + + default: + _tabController.animateTo(0); + } + } else { + _tabController.animateTo(0); + } + return false; + }, + child: Consumer( + builder: (context, state, child) => AnimatedCrossFade( + duration: DesignConfig.lowAnimationDuration, + crossFadeState: state.filtering + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: SizedBox( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + controller: _tabController, + children: const [ + MainPage(), + CategoriesPage(), + NewStatistic(), + ], ), - secondChild: const SearchPage(), ), + secondChild: const SearchPage(), ), ), - bottomNavigationBar: Consumer( - builder: (context, state, child) => DidvanBNB( - currentTabIndex: state.currentPageIndex, - onTabChanged: (index) { + ), + bottomNavigationBar: Consumer( + builder: (context, state, child) => DidvanBNB( + currentTabIndex: state.currentPageIndex, + onTabChanged: (index) { + if (index < _tabController.length) { state.currentPageIndex = index; FocusScope.of(context).unfocus(); state.resetFilters(false); _tabController.animateTo(index); - }, - ), + } + }, ), - ); - } + ), + ); } +} diff --git a/lib/views/home/home_state.dart b/lib/views/home/home_state.dart index dac6a4f..5214bf4 100644 --- a/lib/views/home/home_state.dart +++ b/lib/views/home/home_state.dart @@ -200,7 +200,7 @@ class HomeState extends CoreProvier { MenuItemType( label: 'هوشان', asset: Assets.ai, - link: 'tab-2', + link: Routes.aiSection, ), MenuItemType( label: 'فرصت و تهدید', diff --git a/lib/views/home/main/main_page.dart b/lib/views/home/main/main_page.dart index f59757d..4f85f62 100644 --- a/lib/views/home/main/main_page.dart +++ b/lib/views/home/main/main_page.dart @@ -11,6 +11,7 @@ import 'package:didvan/views/home/main/widgets/banner.dart'; import 'package:didvan/views/home/main/widgets/general_item.dart'; import 'package:didvan/views/home/main/widgets/main_content.dart'; import 'package:didvan/views/home/main/widgets/podcast_item.dart'; +import 'package:didvan/views/home/main/widgets/story_section.dart'; import 'package:didvan/views/widgets/didvan/slider.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; @@ -19,7 +20,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:didvan/views/home/main/widgets/swot_item_card.dart'; +import 'package:didvan/views/home/main/widgets/swot_item_card.dart'; class MainPage extends StatefulWidget { const MainPage({ @@ -45,161 +46,183 @@ class _MainPageState extends State { builder: (context, state) => ListView( padding: const EdgeInsets.symmetric(vertical: 16), children: [ + if (state.stories.isNotEmpty) StorySection(stories: state.stories), + const SizedBox(height: 12), // محتوای اصلی صفحه const MainPageMainContent(), - // آیتم‌های لیست‌ها - ...List.generate(state.content.lists.length + 1, (index) { - if (index == 4) { - return Padding( - padding: const EdgeInsets.only(top: 32), - child: Column( - children: [ + Builder(builder: (context) { + final List pageContent = []; + if (state.content != null && state.content!.lists.isNotEmpty) { + final lists = state.content!.lists; + + for (int i = 0; i < lists.length; i++) { + final currentList = lists[i]; + + if (i == 4) { + pageContent.add( Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, - top: 28, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.only(top: 32), + child: Column( children: [ - const InfoTitle(), - GestureDetector( - onTap: () => { - Navigator.of(context).pushNamed(Routes.infography) - }, + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 16, + top: 28, + ), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - DidvanText( - "همه", - color: Theme.of(context).colorScheme.primary, - ), - Icon( - DidvanIcons.angle_left_light, - color: Theme.of(context).colorScheme.primary, + const InfoTitle(), + GestureDetector( + onTap: () => { + Navigator.of(context) + .pushNamed(Routes.infography) + }, + child: Row( + children: [ + DidvanText( + "همه", + color: Theme.of(context) + .colorScheme + .primary, + ), + Icon( + DidvanIcons.angle_left_light, + color: Theme.of(context) + .colorScheme + .primary, + ) + ], + ), ) ], ), - ) + ), + const MainPageBanner( + isFirst: false, + ), ], ), ), - const MainPageBanner( - isFirst: false, - ), - ], - ), - ); - } + ); + } - int listIndex = index > 4 ? index - 1 : index; - if (listIndex >= state.content.lists.length) { - return const SizedBox.shrink(); - } - return _MainPageSection( - list: state.content.lists[listIndex], - isLast: listIndex == state.content.lists.length - 1, - ); - }), + pageContent.add(_MainPageSection( + list: currentList, + isLast: i == lists.length - 1, + )); - // کانتینر زیر کل لیست‌ها - FutureBuilder>( - future: SwotService.fetchSwotItems(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const SizedBox( - height: 10, - ); - } else if (snapshot.hasError) { - return const SizedBox( - height: 10, - ); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const SizedBox( - height: 10, - ); + if (currentList.type == 'startup') { + pageContent.add(const _SwotSection()); + } } + } + return Column(children: pageContent); + }), + ], + ), + ); + } +} - final items = snapshot.data!; +class _SwotSection extends StatelessWidget { + const _SwotSection(); - return Padding( - padding: const EdgeInsets.all(0.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: SwotService.fetchSwotItems(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 10, + ); + } else if (snapshot.hasError) { + return const SizedBox( + height: 10, + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const SizedBox( + height: 10, + ); + } + + final items = snapshot.data!; + + return Padding( + padding: const EdgeInsets.all(0.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Title Row + Padding( + padding: const EdgeInsets.only(right: 20, top: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - /// Title Row - Padding( - padding: const EdgeInsets.only(right: 20, top: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - "lib/assets/images/features/Saha Solid.svg", - ), - const SizedBox(width: 5), - DidvanText( - "ماژول فرصت و تهدید", - style: Theme.of(context).textTheme.titleMedium, - color: Theme.of(context).colorScheme.title, - ), - ], - ), - GestureDetector( - onTap: () { - AppInitializer.openWebLink( - navigatorKey.currentContext!, - 'http://opportunity-threat.didvan.com/?accessToken=${RequestService.token}', - mode: LaunchMode.inAppWebView, - ); - }, - child: Padding( - padding: const EdgeInsets.only(left: 20), - child: Row( - children: [ - DidvanText( - "همه", - color: - Theme.of(context).colorScheme.primary, - ), - Icon( - DidvanIcons.angle_left_light, - color: - Theme.of(context).colorScheme.primary, - ), - ], - ), - ), - ), - ], - ), + Row( + children: [ + SvgPicture.asset( + "lib/assets/images/features/Saha Solid.svg", + ), + const SizedBox(width: 5), + DidvanText( + "ماژول فرصت و تهدید", + style: Theme.of(context).textTheme.titleMedium, + color: Theme.of(context).colorScheme.title, + ), + ], ), - - const SizedBox(height: 16), - - /// Swot Items Slider - DidvanSlider( - height: 300, - itemCount: items.length, - viewportFraction: 0.65, - itemBuilder: (context, index, realIndex) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 0.0), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SwotItemCard(item: items[index]), + GestureDetector( + onTap: () { + AppInitializer.openWebLink( + navigatorKey.currentContext!, + 'http://opportunity-threat.didvan.com/?accessToken=${RequestService.token}', + mode: LaunchMode.inAppWebView, + ); + }, + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Row( + children: [ + DidvanText( + "همه", + color: Theme.of(context).colorScheme.primary, + ), + Icon( + DidvanIcons.angle_left_light, + color: Theme.of(context).colorScheme.primary, + ), + ], ), ), ), ], ), - ); - }, - ) - ], - ), + ), + + const SizedBox(height: 16), + + /// Swot Items Slider + DidvanSlider( + height: 300, + itemCount: items.length, + viewportFraction: 0.65, + itemBuilder: (context, index, realIndex) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SwotItemCard(item: items[index]), + ), + ), + ), + ], + ), + ); + }, ); } } @@ -362,4 +385,4 @@ class _MainPageSection extends StatelessWidget { ], ); } -} +} \ No newline at end of file diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart index 3582d34..727bb19 100644 --- a/lib/views/home/main/main_page_state.dart +++ b/lib/views/home/main/main_page_state.dart @@ -4,40 +4,61 @@ import 'package:didvan/models/home_page_content/home_page_content.dart'; import 'package:didvan/models/requests/infography.dart'; import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/radar.dart'; +import 'package:didvan/models/story_model.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/services/story_service.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; -// import 'package:url_launcher/url_launcher_string.dart'; class MainPageState extends CoreProvier { - late MainPageContent content; + MainPageContent? content; int unread = 0; + List stories = []; Future _getMainPageContent() async { final service = RequestService(RequestHelper.mainPageContent); - await service.httpGet(); if (service.isSuccess) { content = MainPageContent.fromJson(service.result); unread = service.result['unread']; - appState = AppState.idle; - return; + } else { + throw Exception('Failed to load main page content'); + } + } + + Future _fetchStories() async { + try { + stories = await StoryService.getStories(); + // [اضافه شود] تعداد استوری های دریافت شده را چاپ کنید + print("Fetched ${stories.length} stories."); + } catch (e) { + stories = []; + // [اضافه شود] خطای رخ داده را چاپ کنید + debugPrint("Could not fetch stories: $e"); } - appState = AppState.failed; } void init() { - Future.delayed(Duration.zero, () { - _getMainPageContent(); + Future.delayed(Duration.zero, () async { + appState = AppState.busy; + try { + await Future.wait([ + _getMainPageContent(), + _fetchStories(), + ]); + appState = AppState.idle; + } catch (e) { + appState = AppState.failed; + } }); } void markChangeHandler(String type, int id, bool value) { - content.lists + content?.lists .firstWhere((element) => element.type == type) .contents .firstWhere((element) => element.id == id) @@ -109,4 +130,4 @@ class MainPageState extends CoreProvier { } Navigator.of(navigatorKey.currentContext!).pushNamed(link, arguments: args); } -} +} \ No newline at end of file diff --git a/lib/views/home/main/widgets/banner.dart b/lib/views/home/main/widgets/banner.dart index 79c946e..545f287 100644 --- a/lib/views/home/main/widgets/banner.dart +++ b/lib/views/home/main/widgets/banner.dart @@ -5,7 +5,6 @@ import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; -// import 'package:url_launcher/url_launcher_string.dart'; class MainPageBanner extends StatelessWidget { final bool isFirst; @@ -16,7 +15,7 @@ class MainPageBanner extends StatelessWidget { final state = context.read(); return DidvanSlider( itemBuilder: (context, index, realIndex) { - final item = state.content.banners[isFirst ? 0 : 1][index]; + final item = state.content!.banners[isFirst ? 0 : 1][index]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: GestureDetector( @@ -30,7 +29,7 @@ class MainPageBanner extends StatelessWidget { ), ); }, - itemCount: state.content.banners[isFirst ? 0 : 1].length, + itemCount: state.content!.banners[isFirst ? 0 : 1].length, viewportFraction: 1, enableIndicator: true, height: (MediaQuery.of(context).size.width - 8) * 9.0 / 16.0, diff --git a/lib/views/home/main/widgets/story_section.dart b/lib/views/home/main/widgets/story_section.dart new file mode 100644 index 0000000..d001061 --- /dev/null +++ b/lib/views/home/main/widgets/story_section.dart @@ -0,0 +1,124 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:didvan/models/story_model.dart'; +import 'package:didvan/routes/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class StorySection extends StatelessWidget { + final List stories; + + const StorySection({super.key, required this.stories}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 110.0, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + scrollDirection: Axis.horizontal, + reverse: true, + itemCount: stories.length, + itemBuilder: (BuildContext context, int index) { + final userStories = stories[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: _StoryCircle( + userStories: userStories, + onTap: () { + Navigator.of(context).pushNamed( + Routes.storyViewer, + arguments: { + 'stories': stories, + 'tappedIndex': index, + }, + ); + }, + ), + ); + }, + ), + ); + } +} + +// lib/views/home/main/widgets/story_section.dart + +class _StoryCircle extends StatelessWidget { + final UserStories userStories; + final VoidCallback onTap; + + const _StoryCircle({required this.userStories, required this.onTap}); + + @override + Widget build(BuildContext context) { + final allStoriesViewed = ValueNotifier( + userStories.stories.every((story) => story.isViewed.value)); + + for (var story in userStories.stories) { + story.isViewed.addListener(() { + allStoriesViewed.value = + userStories.stories.every((s) => s.isViewed.value); + }); + } + + return InkWell( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: allStoriesViewed, + builder: (context, isViewed, child) { + return Container( + width: 80.0, + height: 80.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: isViewed + ? const LinearGradient( + colors: [Color.fromARGB(255, 184, 184, 184),Color.fromARGB(255, 184, 184, 184)], + ) + : const LinearGradient( + colors: [ Color.fromARGB(255, 27, 60, 79), Color.fromARGB(255, 27, 60, 79)], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: CircleAvatar( + backgroundColor: const Color.fromARGB(255, 230, 242, 246), + child: ClipOval( + child: SvgPicture.asset( + "lib/assets/icons/MACRO_TRENDS.svg", + fit: BoxFit.cover, + width: 50.0, + height: 50.0, + placeholderBuilder: (context) => const CircularProgressIndicator(strokeWidth: 2.0), + ), + ), + ), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 6.0), + Text( + userStories.user.name, + style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/story_viewer/story_viewer_page.dart b/lib/views/story_viewer/story_viewer_page.dart new file mode 100644 index 0000000..7e388b3 --- /dev/null +++ b/lib/views/story_viewer/story_viewer_page.dart @@ -0,0 +1,406 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:didvan/models/story_model.dart'; +import 'package:didvan/services/story_service.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:video_player/video_player.dart'; + +// Main PageViewer to swipe between users +class StoryViewerPage extends StatefulWidget { + final List stories; + final int tappedIndex; + + const StoryViewerPage({ + super.key, + required this.stories, + required this.tappedIndex, + }); + + @override + State createState() => _StoryViewerPageState(); +} + +class _StoryViewerPageState extends State { + late PageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: widget.tappedIndex); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Directionality( + textDirection: TextDirection.ltr, + child: PageView.builder( + controller: _pageController, + itemCount: widget.stories.length, + itemBuilder: (context, index) { + final userStories = widget.stories[index]; + return UserStoryViewer( + key: ValueKey(userStories.user.name + index.toString()), + userStories: userStories, + onComplete: () { + if (index < widget.stories.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } else { + Navigator.of(context).pop(); + } + }, + ); + }, + ), + ), + ); + } +} + +class UserStoryViewer extends StatefulWidget { + final UserStories userStories; + final VoidCallback onComplete; + + const UserStoryViewer({ + super.key, + required this.userStories, + required this.onComplete, + }); + + @override + State createState() => _UserStoryViewerState(); +} + +class _UserStoryViewerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + VideoPlayerController? _videoController; + int _currentStoryIndex = 0; + bool _isLongPressing = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController(vsync: this); + _loadStory(story: widget.userStories.stories.first); + + _animationController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _nextStory(); + } + }); + } + + @override + void dispose() { + _animationController.dispose(); + _videoController?.dispose(); + super.dispose(); + } + + void _loadStory({required StoryItem story}) { + if (!story.isViewed.value) { + StoryService.markStoryAsViewed(widget.userStories.id, story.id); + story.isViewed.value = true; + } + + _animationController.stop(); + _animationController.reset(); + _videoController?.dispose(); + _videoController = null; + + switch (story.media) { + case MediaType.image: + case MediaType.gif: + _animationController.duration = story.duration; + _animationController.forward(); + break; + case MediaType.video: + _videoController = + VideoPlayerController.networkUrl(Uri.parse(story.url)) + ..initialize().then((_) { + if (mounted) { + setState(() { + if (_videoController!.value.isInitialized) { + // Check for a valid duration + if (_videoController!.value.duration > Duration.zero) { + _animationController.duration = + _videoController!.value.duration; + } else { + // Fallback to default duration if video duration is invalid + _animationController.duration = story.duration; + } + _videoController!.play(); + _animationController.forward(); + } else { + // Fallback for failed initialization + _animationController.duration = story.duration; + _animationController.forward(); + } + }); + } + }); + break; + } + setState(() {}); + } + + void _nextStory() { + if (_currentStoryIndex < widget.userStories.stories.length - 1) { + setState(() { + _currentStoryIndex++; + }); + _loadStory(story: widget.userStories.stories[_currentStoryIndex]); + } else { + widget.onComplete(); + } + } + + void _previousStory() { + if (_currentStoryIndex > 0) { + setState(() { + _currentStoryIndex--; + }); + _loadStory(story: widget.userStories.stories[_currentStoryIndex]); + } + } + + void _pauseStory() { + _animationController.stop(); + _videoController?.pause(); + } + + void _resumeStory() { + _animationController.forward(); + _videoController?.play(); + } + + void _handleTap(TapUpDetails details) { + if (_isLongPressing) { + _isLongPressing = false; + _resumeStory(); + return; + } + final double screenWidth = MediaQuery.of(context).size.width; + final double dx = details.globalPosition.dx; + + if (dx > screenWidth / 2) { + _nextStory(); + } else { + _previousStory(); + } + } + + @override + Widget build(BuildContext context) { + final story = widget.userStories.stories[_currentStoryIndex]; + return Scaffold( + backgroundColor: Colors.black, + body: GestureDetector( + onTapUp: _handleTap, + onLongPressStart: (_) { + _isLongPressing = true; + _pauseStory(); + }, + onLongPressEnd: (_) { + _isLongPressing = false; + _resumeStory(); + }, + child: Stack( + fit: StackFit.expand, + children: [ + _buildMediaViewer(story), + _buildStoryHeader(), + ], + ), + ), + ); + } + + Widget _buildMediaViewer(StoryItem story) { + switch (story.media) { + case MediaType.image: + return CachedNetworkImage( + imageUrl: story.url, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity); + case MediaType.gif: + return Image.network(story.url, + fit: BoxFit.cover, width: double.infinity, height: double.infinity); + case MediaType.video: + if (_videoController?.value.isInitialized ?? false) { + return FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: _videoController!.value.size.width, + height: _videoController!.value.size.height, + child: VideoPlayer(_videoController!))); + } + return const Center(child: CircularProgressIndicator()); + } + } + + Widget _buildStoryHeader() { + return Positioned( + top: 35.0, + left: 10.0, + right: 10.0, + child: Column( + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: widget.userStories.stories + .asMap() + .map((i, e) { + return MapEntry( + i, + _AnimatedBar( + animationController: _animationController, + position: i, + currentIndex: _currentStoryIndex, + ), + ); + }) + .values + .toList(), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: _UserInfo(user: widget.userStories.user), + ), + ], + ), + ); + } +} + +class _AnimatedBar extends StatelessWidget { + final AnimationController animationController; + final int position; + final int currentIndex; + + const _AnimatedBar({ + required this.animationController, + required this.position, + required this.currentIndex, + }); + + @override + Widget build(BuildContext context) { + return Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.5), + child: LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + Container( + height: 6.0, + width: double.infinity, + decoration: BoxDecoration( + color: position < currentIndex + ? Colors.white + : Colors.white.withOpacity(0.5), + border: Border.all(color: Colors.black26, width: 0.8), + borderRadius: BorderRadius.circular(30.0), + ), + ), + if (position == currentIndex) + Align( + alignment: Alignment.centerLeft, + child: AnimatedBuilder( + animation: animationController, + builder: (context, child) { + return Container( + height: 6.0, + width: + constraints.maxWidth * animationController.value, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30.0), + ), + ); + }, + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _UserInfo extends StatelessWidget { + final User user; + + const _UserInfo({required this.user}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + CircleAvatar( + radius: 20.0, + backgroundColor: Colors.grey[300], + child: ClipOval( + child: Padding( + padding: const EdgeInsets.all(5.0), + child: SvgPicture.asset( + user.profileImageUrl, + width: 40.0, + height: 40.0, + fit: BoxFit.cover, + placeholderBuilder: (context) => const CircularProgressIndicator( + strokeWidth: 2.0, + ), + ), + ), + ), + ), + const SizedBox(width: 10.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user.name, + style: const TextStyle( + color: Colors.white, + fontSize: 18.0, + fontWeight: FontWeight.w600, + ), + ), + DidvanText( + DateTimeUtils.momentGenerator(user.createdAt), + style: const TextStyle( + color: Colors.white70, + fontSize: 14.0, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 30.0, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/views/widgets/ai_banner.dart b/lib/views/widgets/ai_banner.dart index 2efb873..063087a 100644 --- a/lib/views/widgets/ai_banner.dart +++ b/lib/views/widgets/ai_banner.dart @@ -15,8 +15,8 @@ class AiBanner extends StatelessWidget { NativeWebViewLauncher.openWebView( 'https://www.aisada.ir/app/page1.html'); }, - child: Padding( - padding: const EdgeInsets.only(top: 20), + child: const Padding( + padding: EdgeInsets.only(top: 20), child: Stack( children: [ Icon(Icons.insert_comment_sharp), diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart index 9569ef5..9f557a0 100644 --- a/lib/views/widgets/didvan/bnb.dart +++ b/lib/views/widgets/didvan/bnb.dart @@ -1,6 +1,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; @@ -58,18 +59,18 @@ class DidvanBNB extends StatelessWidget { onTap: () => onTabChanged(1), ), _NavBarItem( - isSelected: currentTabIndex == 2, + isSelected: false, title: 'هوشان', selectedIcon: DidvanIcons.ai_solid, unselectedIcon: DidvanIcons.ai_regular, - onTap: () => onTabChanged(2), + onTap: () => Navigator.of(context).pushNamed(Routes.aiSection), ), _NavBarItem( - isSelected: currentTabIndex == 3, + isSelected: currentTabIndex == 2, title: 'آمار و داده', selectedIcon: DidvanIcons.stats__solid, unselectedIcon: DidvanIcons.stats__light, - onTap: () => onTabChanged(3), + onTap: () => onTabChanged(2), ), ], ), @@ -145,4 +146,4 @@ class _NavBarItem extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/views/widgets/didvan/text_field.dart b/lib/views/widgets/didvan/text_field.dart index 632e7e2..78c3a86 100644 --- a/lib/views/widgets/didvan/text_field.dart +++ b/lib/views/widgets/didvan/text_field.dart @@ -20,7 +20,7 @@ class DidvanTextField extends StatefulWidget { final bool acceptSpace; final String? Function(String value)? validator; final TextInputType? textInputType; - final TextInputAction? textInputAction; // پارامتر جدید + final TextInputAction? textInputAction; final bool disableBorders; final bool isSmall; final int? maxLine; @@ -38,7 +38,7 @@ class DidvanTextField extends StatefulWidget { this.initialValue, this.validator, this.textInputType, - this.textInputAction, // اضافه کردن به سازنده + this.textInputAction, this.textAlign, this.obsecureText = false, this.autoFocus = false, @@ -111,13 +111,13 @@ class _DidvanTextFieldState extends State { inputFormatters: [ if (!widget.acceptSpace) FilteringTextInputFormatter.allow( - RegExp("[0-9a-zA-Z]")), + RegExp("[0-9a-zA-Z\u0600-\u06FF]")), ], autofocus: widget.autoFocus, obscureText: _hideContent, textAlign: widget.textAlign ?? TextAlign.start, keyboardType: widget.textInputType, - textInputAction: widget.textInputAction, // پاس دادن به TextFormField + textInputAction: widget.textInputAction, focusNode: _focusNode, controller: _controller, onFieldSubmitted: widget.onSubmitted, diff --git a/pubspec.yaml b/pubspec.yaml index b7a373a..fc543d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -143,6 +143,7 @@ flutter: - lib/assets/icons/ - lib/assets/animations/ - lib/assets/js/ + - lib/assets/icons/houshanNav/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.