From 0581da038a108255c181144ba3df33838a55d5fb Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 16 Jun 2025 17:33:17 +0300 Subject: [PATCH 01/13] feat(chat): Issue-440 add photo/file picker, fix black screen --- lib/core/app_router.dart | 36 +++--- .../chat/presentation/pages/chat_screen.dart | 107 +++++++++++++----- .../feed/presentation/pages/feed_screen.dart | 2 + .../internet_connection_checker_screen.dart | 2 + lib/main.dart | 1 - 5 files changed, 102 insertions(+), 46 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 0bd17c58..7291ed95 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -166,7 +166,7 @@ class AppRouter { await sl().post( '/api/profile/notification/token/register', data: { - 'platform': Platform.isIOS ? 'ios': 'android', + 'platform': Platform.isIOS ? 'ios' : 'android', 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'token': fbToken, }, @@ -175,12 +175,16 @@ class AppRouter { } catch (e) { rethrow; } - final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed'); - if (welcomeScreenWasShowed == true) { + try { + final sharedPreferences = await SharedPreferences.getInstance(); + final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed'); + if (welcomeScreenWasShowed == true) { + return '/feed-screen'; + } else { + return null; + } + } catch (e) { return '/feed-screen'; - } else { - return null; } }, ), @@ -189,14 +193,18 @@ class AppRouter { name: 'updateScreen', builder: (context, state) => const UpdateScreen(), redirect: (context, state) async { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - Version currentVersion = Version.parse(version); - Version latestVersion = Version.parse(versions['min_version']); - if (latestVersion > currentVersion) { - return null; - } else { + try { + final versions = (await sl().get('$baseUrl/api/app_versions')).data; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version; + Version currentVersion = Version.parse(version); + Version latestVersion = Version.parse(versions['min_version']); + if (latestVersion > currentVersion) { + return null; + } else { + return '/welcome-screen'; + } + } catch (e) { return '/welcome-screen'; } }, diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 5ec98c6c..3f7f1cdd 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -21,15 +21,18 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String chatId; final bool? sendInitialMessage; + const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage}); @override @@ -338,37 +341,7 @@ class _ChatScreenState extends State { children: [ const SizedBox(width: 8), InkWell( - onTap: () async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - allowMultiple: true, - type: FileType.custom, - allowedExtensions: [ - 'pdf', - 'doc', - 'docx', - 'json', - 'ppt', - 'pptx', - 'rtf', - 'txt', - 'xls', - 'xlsx', - 'csv', - 'jpeg', - 'jpg', - 'png', - 'gif', - 'svg', - 'xml', - 'webp', - ], - ); - - if (result != null) { - List files = result.paths.map((path) => File(path!)).toList(); - await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files); - } - }, + onTap: () => showImagePicker(), customBorder: const CircleBorder(), child: Ink( height: 40, @@ -448,6 +421,78 @@ class _ChatScreenState extends State { ); } + showImagePicker() { + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + final picker = ImagePicker(); + XFile? image = await picker.pickImage(source: ImageSource.camera); + if (image != null) { + await chatRoomCubit.sendMessage(chatId: widget.chatId, files: [File(image.path)]); + } + }, + child: const Row( + children: [ + Icon(Icons.photo_camera), + SizedBox(width: 8), + Text('Фото'), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: [ + 'pdf', + 'doc', + 'docx', + 'json', + 'ppt', + 'pptx', + 'rtf', + 'txt', + 'xls', + 'xlsx', + 'csv', + 'jpeg', + 'jpg', + 'png', + 'gif', + 'svg', + 'xml', + 'webp', + ], + ); + if (result != null) { + List files = result.paths.map((path) => File(path!)).toList(); + await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files); + } + }, + child: const Row( + children: [ + Icon(Icons.file_present_rounded), + SizedBox(width: 8), + Text('Файлы'), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ), + ); + } + _messageDate(DateTime date) { return Center( child: Padding( diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 0fb3cc3d..a18cb9a7 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -21,6 +21,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; import '../widgets/recipes.dart'; @@ -100,6 +101,7 @@ class _FeedScreenContentState extends State { @override Widget build(BuildContext context) { + FlutterNativeSplash.remove(); return Scaffold( floatingActionButton: _selectedChipIndex == 1 ? FloatingActionButton.small( diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart index 0f7f6e2c..3aa1bc34 100644 --- a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; @@ -116,6 +117,7 @@ class NoInternetScreen extends StatelessWidget { final Future Function() onTryAgain; @override Widget build(BuildContext context) { + FlutterNativeSplash.remove(); return Scaffold( body: Column( children: [ diff --git a/lib/main.dart b/lib/main.dart index d7d37793..1616d9eb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,6 @@ Future main() async { options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; options.tracesSampleRate = 1.0; }, appRunner: () => runApp(const MyApp())); - FlutterNativeSplash.remove(); } class MyApp extends StatelessWidget { From ca296040f2e7e821f9b2911e95b7886308f5a631 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 18 Jun 2025 09:55:33 +0300 Subject: [PATCH 02/13] fix: Issue-514 and Issue-497 --- .../repositories/service_repository.dart | 32 +++++++++++++++++++ .../settings/services/settings_service.dart | 10 ++++++ .../presentation/update_screen.dart | 2 ++ .../presentation/pages/welcome_screen.dart | 2 ++ 4 files changed, 46 insertions(+) diff --git a/lib/features/settings/repositories/service_repository.dart b/lib/features/settings/repositories/service_repository.dart index eb462de3..e1e71f09 100644 --- a/lib/features/settings/repositories/service_repository.dart +++ b/lib/features/settings/repositories/service_repository.dart @@ -38,4 +38,36 @@ class SettingsRepository { rethrow; } } + + Future sendEmailCodeForPasswordChange(String email) async { + try { + return await settingsService.sendEmailCodeForPasswordChange( + data: { + 'email': email, + }, + ); + } catch (e) { + rethrow; + } + } + + Future confirmPasswordChange( + String code, + String password, + String passwordConfirmation, + String email, + ) async { + try { + return await settingsService.confirmPasswordChange( + data: { + 'code': code, + 'password': password, + 'password_confirmation': passwordConfirmation, + 'email': email, + }, + ); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/settings/services/settings_service.dart b/lib/features/settings/services/settings_service.dart index d17b5842..e4cdb6b7 100644 --- a/lib/features/settings/services/settings_service.dart +++ b/lib/features/settings/services/settings_service.dart @@ -21,4 +21,14 @@ abstract class SettingsService { Future updateProfileDetails({ @Body() required Map data, }); + + @POST('/api/send_password_reset_code') + Future sendEmailCodeForPasswordChange({ + @Body() required Map data, + }); + + @POST('/api/confirm_password_reset_code') + Future confirmPasswordChange({ + @Body() required Map data, + }); } diff --git a/lib/features/update_screen/presentation/update_screen.dart b/lib/features/update_screen/presentation/update_screen.dart index 99e9e880..186987c2 100644 --- a/lib/features/update_screen/presentation/update_screen.dart +++ b/lib/features/update_screen/presentation/update_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -11,6 +12,7 @@ class UpdateScreen extends StatelessWidget { @override Widget build(BuildContext context) { + FlutterNativeSplash.remove(); return Scaffold( body: Stack( clipBehavior: Clip.none, diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 70a5904e..25922a10 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../../core/theme/theme_helper.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; class WelcomeScreen extends StatefulWidget { const WelcomeScreen({super.key}); @@ -23,6 +24,7 @@ class _WelcomeScreenState extends State { @override Widget build(BuildContext context) { + FlutterNativeSplash.remove(); return Scaffold( body: Stack( clipBehavior: Clip.none, From 7682859d3208fd5eede01a9d3720f2d4b0538842 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 18 Jun 2025 16:56:42 +0300 Subject: [PATCH 03/13] fix: Issue-514 BlackScreens + info for apple --- android/app/src/main/AndroidManifest.xml | 1 + ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 4 +- lib/core/app_router.dart | 15 +- .../auth/presentation/pages/auth_screen.dart | 24 ++- .../widgets/bottom_navigation_bar_widget.dart | 4 +- .../widgets/blog_post_widget.dart | 144 +++++++++--------- .../presentation/widgets/comments_widget.dart | 6 +- .../presentation/pages/profile_screen.dart | 9 +- .../widgets/profile_blog_post_widget.dart | 7 +- .../presentation/pages/welcome_screen.dart | 10 +- lib/main.dart | 16 +- pubspec.yaml | 2 +- 13 files changed, 138 insertions(+), 110 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c7878e41..d8c4ec86 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ CFBundleSignature ???? CFBundleVersion - 27 + 28 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS @@ -31,6 +31,8 @@ NSAllowsArbitraryLoads + NSAppleMusicUsageDescription + This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us. NSCameraUsageDescription Предоставление доступа позволит Вам делать фотографии и добавлять к записям NSLocationWhenInUseUsageDescription diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 7291ed95..d8da4a42 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -158,6 +158,7 @@ class AppRouter { name: 'welcomeScreen', builder: (context, state) => const WelcomeScreen(), redirect: (context, state) async { + try { if (await TokenStorage.haveTokenInStorage()) { final firebaseApi = sl(); @@ -172,19 +173,17 @@ class AppRouter { }, ); } - } catch (e) { - rethrow; - } - try { final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed'); - if (welcomeScreenWasShowed == true) { + final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + if (welcomeScreenWasShowed == false) { + return null; + } else if (await TokenStorage.haveTokenInStorage()) { return '/feed-screen'; } else { - return null; + return '/auth-screen'; } } catch (e) { - return '/feed-screen'; + rethrow; } }, ), diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 5455d5fd..179e0974 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -52,15 +52,23 @@ class _AuthScreenState extends State { } Future _reconnectWebSocket(BuildContext context) async { - await sl().clear(); - await sl().initialize(); + try { + await sl().clear(); + await sl().initialize(); + } catch (e) { + rethrow; + } if (context.mounted) { - context.read().getMyProfile(); - context.read().getChatRoomsWithLoading(); - context.read().connectToWebSocket(); - context.read().getNotifications(); - context.read().connectToWebSocket(); - showSuccessToast('Успешный вход'); + try { + context.read().getMyProfile(); + context.read().getChatRoomsWithLoading(); + context.read().connectToWebSocket(); + context.read().getNotifications(); + context.read().connectToWebSocket(); + showSuccessToast('Успешный вход'); + } catch (e) { + rethrow; + } GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } } diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index 0fae58a6..e370452a 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -82,7 +82,9 @@ class _BottomNavigationBarWidgetState extends State { BottomNavigationBarItem( label: '', icon: CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl), + foregroundImage: NetworkImage( + widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl, + ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 16, ), diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index bc7faec0..b349f8fa 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -56,81 +56,85 @@ class _BlogPostWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - GoRouter.of(context).pushNamed( - 'profileScreen', - extra: Author( - authorSlug: state.feedBlogPostList[widget.index].author.authorSlug, - authorName: state.feedBlogPostList[widget.index].author.authorName, - authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon, - authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe, - ), - ); - }, - child: CircleAvatar( - radius: 20, - backgroundColor: Colors.black12, - foregroundImage: NetworkImage( - baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, - ), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - ), - ), - const SizedBox(width: 8), - // Имя и дата внутри Expanded - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - state.feedBlogPostList[widget.index].author.authorName, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - height: 1, - color: Colors.black87, - fontWeight: FontWeight.w500, - fontSize: 16, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + GoRouter.of(context).pushNamed( + 'profileScreen', + extra: Author( + authorSlug: state.feedBlogPostList[widget.index].author.authorSlug, + authorName: state.feedBlogPostList[widget.index].author.authorName, + authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') + ? state.feedBlogPostList[widget.index].author.authorProfileIcon + : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe, ), + ); + }, + child: CircleAvatar( + radius: 20, + backgroundColor: Colors.black12, + foregroundImage: NetworkImage( + state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') + ? state.feedBlogPostList[widget.index].author.authorProfileIcon + : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, ), - const SizedBox(height: 4), - Text( - '${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}', - style: const TextStyle( - height: 1, - color: Colors.black45, - fontSize: 14, - ), - ), - ], - ), - ), - // Кнопка подписки (если нужно) - if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && - !state.feedBlogPostList[widget.index].author.authorIsMe) - Padding( - padding: const EdgeInsets.only(right: 8), - child: SubscribeButtonWidget( - slug: state.feedBlogPostList[widget.index].author.authorSlug, + backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), ), - // Кнопка меню - CustomPopup( - isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe, - complainedContentType: ComplainedContent.posts, - path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}', - id: state.feedBlogPostList[widget.index].id, - feedBlogPostModel: state.feedBlogPostList[widget.index], - ), - ], + const SizedBox(width: 8), + // Имя и дата внутри Expanded + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.feedBlogPostList[widget.index].author.authorName, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1, + color: Colors.black87, + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + '${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}', + style: const TextStyle( + height: 1, + color: Colors.black45, + fontSize: 14, + ), + ), + ], + ), + ), + // Кнопка подписки (если нужно) + if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) && + !state.feedBlogPostList[widget.index].author.authorIsMe) + Padding( + padding: const EdgeInsets.only(right: 8), + child: SubscribeButtonWidget( + slug: state.feedBlogPostList[widget.index].author.authorSlug, + ), + ), + // Кнопка меню + CustomPopup( + isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe, + complainedContentType: ComplainedContent.posts, + path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}', + id: state.feedBlogPostList[widget.index].id, + feedBlogPostModel: state.feedBlogPostList[widget.index], + ), + ], + ), ), - ), - if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[ + if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index 16f52f90..1e1a90d5 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -72,13 +72,15 @@ class _CommentsWidgetState extends State { extra: Author( authorSlug: comment.authorSlug, authorName: comment.authorName, - authorProfileIcon: comment.authorProfileIcon, + authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, authorIsMe: (sl().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false, ), ); }, child: CircleAvatar( - foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon), + foregroundImage: NetworkImage( + comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, + ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 363c2152..70bd7e84 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -600,9 +600,7 @@ class _ProfileScreenState extends State { _educationBlock(String title, String date) { final parsedDate = DateTime.tryParse(date); - final formattedDate = parsedDate != null - ? DateFormat('d MMMM y', 'ru_RU').format(parsedDate) - : date; + final formattedDate = parsedDate != null ? DateFormat('d MMMM y', 'ru_RU').format(parsedDate) : date; return SizedBox( width: double.infinity, @@ -640,7 +638,6 @@ class _ProfileScreenState extends State { ); } - _socialLinks(ProfileStateLoaded state) { if (!state.profile.roles.contains('consultant')) { return const SizedBox.shrink(); @@ -1065,7 +1062,9 @@ class _ProfileScreenState extends State { tag: state.profile.profilePhotoUrl, child: CircleAvatar( radius: 32, - foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl), + foregroundImage: NetworkImage( + state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : baseUrl + state.profile.profilePhotoUrl, + ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), ), diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 18f2a878..5ac4d60e 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -67,7 +67,9 @@ class _ProfileBlogPostWidgetState extends State { radius: 20, backgroundColor: Colors.black12, foregroundImage: NetworkImage( - baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, + state.blogPosts[widget.index].author.authorProfileIcon.contains('https') + ? state.blogPosts[widget.index].author.authorProfileIcon + : baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -119,8 +121,7 @@ class _ProfileBlogPostWidgetState extends State { ), ], if (state.blogPosts[widget.index].title.isEmpty) const SizedBox(height: 8), - if (state.blogPosts[widget.index].title.isNotEmpty && state.blogPosts[widget.index].description.isNotEmpty) - const SizedBox(height: 8), + if (state.blogPosts[widget.index].title.isNotEmpty && state.blogPosts[widget.index].description.isNotEmpty) const SizedBox(height: 8), if (state.blogPosts[widget.index].description.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 4), diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 25922a10..31b4bf0f 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -1,9 +1,9 @@ +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; - -import '../../../../core/theme/theme_helper.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; class WelcomeScreen extends StatefulWidget { @@ -82,7 +82,11 @@ class _WelcomeScreenState extends State { onTap: () async { final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setBool('welcomeScreenWasShowed', true); - goRouter.go('/feed-screen'); + if (await TokenStorage.haveTokenInStorage()) { + goRouter.go('/feed-screen'); + } else { + goRouter.go('/auth-screen'); + } }, borderRadius: BorderRadius.circular(8), child: Ink( diff --git a/lib/main.dart b/lib/main.dart index 1616d9eb..a712f6e3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,14 +20,20 @@ import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; Future main() async { - WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await di.init(); - await SentryFlutter.init((options) { - options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; - options.tracesSampleRate = 1.0; - }, appRunner: () => runApp(const MyApp())); + + await SentryFlutter.init( + (options) { + options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; + options.tracesSampleRate = 1.0; + options.debug = false; + }, + appRunner: () => runApp(const MyApp()), + ); } class MyApp extends StatelessWidget { diff --git a/pubspec.yaml b/pubspec.yaml index c9e7430b..0a1982b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.4+34 +version: 1.1.5+35 publish_to: 'none' environment: From 3b25e30b41970c20808ff40e6be2a56158892962 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 20 Jun 2025 11:38:31 +0300 Subject: [PATCH 04/13] fix(chat): Issue-440 chat v2 --- lib/core/locator_service.dart | 1 + .../chat/repositories/chat_repository.dart | 21 +++++++++++++++ lib/features/chat/services/chat_service.dart | 15 ++++++++++- .../bloc/profile_cubit/profile_cubit.dart | 26 +++++++++++++++++-- .../presentation/pages/profile_screen.dart | 9 +------ 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index accb729d..9395a49f 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -226,6 +226,7 @@ Future init() async { 'accept': 'application/json', 'content-type': 'application/json', }, + validateStatus: (status) => status != null && status < 500, ), )..interceptors.addAll( [ diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 28f80a48..4d1e9214 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -154,4 +154,25 @@ class ChatRepository { rethrow; } } + + Future getChatByUserSlug(String slug) async { + try { + final response = await chatService.getChatByUserSlug(slug: slug); + if (response.response.statusCode == 404) { + return '404'; + } else { + return response.data['id']; + } + } catch (e) { + return 'error'; + } + } + + Future sendFirstChatMessageForCreateChatRoom(String userSlug) async { + try { + return await chatService.sendMessageByUserSlug(userSlug: userSlug, data: 'Создал чат!'); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index 6d4c992e..dcdab395 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -24,7 +24,7 @@ abstract class ChatService { @Path() required String id, }); - @POST('/api/chat/{id}/message/send') + @POST('/api/v2/chat/message/send/{id}') @MultiPart() Future sendMessage({ @Path() required String id, @@ -80,4 +80,17 @@ abstract class ChatService { @Path() required int id, @Body() required PlanComplainModel body, }); + + @GET('/api/v2/chat/get_by_user/{slug}') + Future getChatByUserSlug({ + @Path() required String slug, + }); + + @POST('/api/v2/chat/message/send/{userSlug}') + @MultiPart() + Future sendMessageByUserSlug({ + @Path() required String userSlug, + @Part() String? data, + @Part(name: 'media_data[]', contentType: 'multipart/form-data') List? mediaData, + }); } diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 28f654f2..530bc9aa 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -1,12 +1,15 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class ProfileCubit extends Cubit { final ProfileRepository profileRepository; @@ -103,11 +106,30 @@ class ProfileCubit extends Cubit { } } - Future getChatRoomIdBySlug({ + getChatRoomIdBySlug({ required String slug, }) async { try { - return (await chatRepository.getChatRoomIdByUserSlug(slug: slug)).id; + final chatIdOrError = await chatRepository.getChatByUserSlug(slug); + if (chatIdOrError == '404') { + await chatRepository.sendFirstChatMessageForCreateChatRoom(slug); + final chatIdOrError = await chatRepository.getChatByUserSlug(slug); + AppRouter.navigatorKey.currentContext?.pushNamed( + 'chatScreen', + extra: { + 'chatId': chatIdOrError, + }, + ); + } else if (chatIdOrError == 'error') { + showErrorToast('Ошибка создания чата'); + } else { + AppRouter.navigatorKey.currentContext?.pushNamed( + 'chatScreen', + extra: { + 'chatId': chatIdOrError, + }, + ); + } } catch (e, st) { log(e.toString(), stackTrace: st); return ''; diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 2eaa6d90..a847e697 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1232,14 +1232,7 @@ class _ProfileScreenState extends State { children: [ const SizedBox(width: 16), GestureDetector( - onTap: () async { - GoRouter.of(context).pushNamed( - 'chatScreen', - extra: { - 'chatId': await profileCubit.getChatRoomIdBySlug(slug: widget.author!.authorSlug), - }, - ); - }, + onTap: () async => await profileCubit.getChatRoomIdBySlug(slug: widget.author!.authorSlug), child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), From 84b88282ad089a5d5a446dea19920d1b1d75d4b1 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 20 Jun 2025 14:12:54 +0300 Subject: [PATCH 05/13] feat(auth): reset password --- lib/core/app_router.dart | 13 +- lib/core/locator_service.dart | 8 +- .../bloc/{ => auth_cubit}/auth_cubit.dart | 2 +- .../bloc/{ => auth_cubit}/auth_state.dart | 0 .../reset_password_cubit.dart | 35 ++++ .../reset_password_state.dart | 22 +++ .../auth/presentation/pages/auth_screen.dart | 18 +- .../pages/forget_password_screen.dart | 165 ++++++++++++++++++ .../reset_password_repository.dart | 53 ++++++ .../auth/services/reset_password_service.dart | 19 ++ .../pages/webview_registration_screen.dart | 2 +- .../repositories/service_repository.dart | 32 ---- .../settings/services/settings_service.dart | 10 -- pubspec.yaml | 2 +- 14 files changed, 330 insertions(+), 51 deletions(-) rename lib/features/auth/presentation/bloc/{ => auth_cubit}/auth_cubit.dart (97%) rename lib/features/auth/presentation/bloc/{ => auth_cubit}/auth_state.dart (100%) create mode 100644 lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart create mode 100644 lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart create mode 100644 lib/features/auth/presentation/pages/forget_password_screen.dart create mode 100644 lib/features/auth/repositories/reset_password_repository.dart create mode 100644 lib/features/auth/services/reset_password_service.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 3d0ffb19..a07e12ba 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -71,6 +72,7 @@ import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:version/version.dart'; +import 'dart:developer'; class AppRouter { static GlobalKey navigatorKey = GlobalKey(); @@ -89,6 +91,11 @@ class AppRouter { name: 'registrationScreen', builder: (context, state) => const RegistrationScreen(), ), + GoRoute( + path: 'forget-password-screen', + name: 'forgetPasswordScreen', + builder: (context, state) => const ForgetPasswordScreen(), + ), ], ), ShellRoute( @@ -117,10 +124,10 @@ class AppRouter { redirect: (context, state) async { try { await sl().initialize(); - } catch (e) { - rethrow; + } catch (e, st) { + log(e.toString(), stackTrace: st); } - return '/feed-screen'; + return null; }, ), GoRoute( diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9395a49f..d912f826 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,9 +5,12 @@ import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; @@ -158,6 +161,7 @@ Future init() async { sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -186,6 +190,7 @@ Future init() async { sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -216,6 +221,7 @@ Future init() async { sl.registerLazySingleton(() => NotificationsService(sl())); sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => ResetPasswordService(sl())); // External sl.registerLazySingleton( diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart similarity index 97% rename from lib/features/auth/presentation/bloc/auth_cubit.dart rename to lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 3c5e30af..20686cce 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/auth/presentation/bloc/auth_state.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_state.dart similarity index 100% rename from lib/features/auth/presentation/bloc/auth_state.dart rename to lib/features/auth/presentation/bloc/auth_cubit/auth_state.dart diff --git a/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart new file mode 100644 index 00000000..37fd0d5a --- /dev/null +++ b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart @@ -0,0 +1,35 @@ +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ResetPasswordCubit extends Cubit { + final ResetPasswordRepository resetPasswordRepository; + + ResetPasswordCubit({required this.resetPasswordRepository}) : super(const ResetPasswordState.empty()); + + trySendEmailCode(String email) async { + emit(const ResetPasswordState.loadingSendEmailCode()); + try { + if (await resetPasswordRepository.sendEmailCodeForPasswordChange(email)) { + emit(const ResetPasswordState.codeSendSuccess()); + } else { + emit(const ResetPasswordState.codeSendError()); + } + } catch (e) { + emit(const ResetPasswordState.codeSendError()); + } + } + + tryResetPassword(String email, String code, String password, String passwordConfirmation) async { + emit(const ResetPasswordState.loadingResetPassword()); + try { + if (await resetPasswordRepository.confirmPasswordChange(code, password, passwordConfirmation, email)) { + emit(const ResetPasswordState.resetPasswordSuccess()); + } else { + emit(const ResetPasswordState.resetPasswordError()); + } + } catch (e) { + emit(const ResetPasswordState.resetPasswordError()); + } + } +} diff --git a/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart new file mode 100644 index 00000000..a936ccaa --- /dev/null +++ b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'reset_password_state.freezed.dart'; + +@freezed +abstract class ResetPasswordState with _$ResetPasswordState { + const factory ResetPasswordState() = _ResetPasswordState; + + const factory ResetPasswordState.empty() = ResetPasswordStateEmpty; + + const factory ResetPasswordState.loadingSendEmailCode() = ResetPasswordStateLoadingSendEmailCode; + + const factory ResetPasswordState.codeSendSuccess() = ResetPasswordStateCodeSendSuccess; + + const factory ResetPasswordState.codeSendError() = ResetPasswordStateCodeSendError; + + const factory ResetPasswordState.loadingResetPassword() = ResetPasswordStateLoadingResetPassword; + + const factory ResetPasswordState.resetPasswordError() = ResetPasswordStateResetPasswordError; + + const factory ResetPasswordState.resetPasswordSuccess() = ResetPasswordStateResetPassowrdSuccess; +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 179e0974..123eac0f 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -6,8 +6,8 @@ import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; @@ -135,6 +135,20 @@ class _AuthScreenState extends State { ), ), const SizedBox(height: 16), + Center( + child: GestureDetector( + onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'), + child: Text( + 'Забыли пароль?', + style: TextStyle( + color: appTheme.blueA400, + decorationColor: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + ), + ), + ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/auth/presentation/pages/forget_password_screen.dart b/lib/features/auth/presentation/pages/forget_password_screen.dart new file mode 100644 index 00000000..78313a15 --- /dev/null +++ b/lib/features/auth/presentation/pages/forget_password_screen.dart @@ -0,0 +1,165 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class ForgetPasswordScreen extends StatefulWidget { + const ForgetPasswordScreen({super.key}); + + @override + State createState() => _ForgetPasswordScreenState(); +} + +class _ForgetPasswordScreenState extends State { + final emailController = TextEditingController(); + final codeController = TextEditingController(); + final passwordController = TextEditingController(); + final passwordConfirmController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => sl(), + child: BlocConsumer( + builder: (BuildContext context, ResetPasswordState state) { + return Scaffold( + appBar: AppBar( + title: const Text('Смена пароля'), + centerTitle: false, + ), + body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode + ? const Center( + child: CircularProgressIndicator(), + ) + : Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[ + const Text( + 'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + const Text( + 'E-mail', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: emailController, + hintText: 'Введите e-mail', + ), + ], + if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[ + const Text( + 'Код', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: codeController, + hintText: 'Введите код', + ), + const SizedBox(height: 12), + const Text( + 'Пароль', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: passwordController, + hintText: 'Введите новый пароль', + ), + const SizedBox(height: 12), + const Text( + 'Подтвердите пароль', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: passwordConfirmController, + hintText: 'Введите новый пароль', + ), + ], + const Spacer(), + InkWell( + onTap: () { + if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) { + context.read().tryResetPassword( + emailController.text, + codeController.text, + passwordController.text, + passwordConfirmController.text, + ); + } else { + context.read().trySendEmailCode(emailController.text); + } + }, + borderRadius: BorderRadius.circular(20), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: appTheme.blueA400, + ), + child: Center( + child: Text( + state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError + ? 'Сменить пароль' + : 'Сбросить пароль', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + listener: (BuildContext context, ResetPasswordState state) { + if (state is ResetPasswordStateCodeSendSuccess) { + showSuccessToast('Код на почту успешно отправлен'); + } + if (state is ResetPasswordStateCodeSendError) { + showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты'); + } + if (state is ResetPasswordStateResetPasswordError) { + showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля'); + } + if (state is ResetPasswordStateResetPassowrdSuccess) { + GoRouter.of(context).pop(); + showSuccessToast('Пароль успешно сменён. Войдите в аккаунт'); + } + }, + ), + ); + } +} diff --git a/lib/features/auth/repositories/reset_password_repository.dart b/lib/features/auth/repositories/reset_password_repository.dart new file mode 100644 index 00000000..e0b1d69f --- /dev/null +++ b/lib/features/auth/repositories/reset_password_repository.dart @@ -0,0 +1,53 @@ +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; + +class ResetPasswordRepository { + final ResetPasswordService resetPasswordService; + + ResetPasswordRepository({ + required this.resetPasswordService, + }); + + Future sendEmailCodeForPasswordChange(String email) async { + try { + final response = await resetPasswordService.sendEmailCodeForPasswordChange( + data: { + 'email': email, + }, + ); + int statusCode = response.response.statusCode ?? 0; + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + return false; + } + } catch (e) { + rethrow; + } + } + + Future confirmPasswordChange( + String code, + String password, + String passwordConfirmation, + String email, + ) async { + try { + final response = await resetPasswordService.confirmPasswordChange( + data: { + 'code': code, + 'password': password, + 'password_confirmation': passwordConfirmation, + 'email': email, + }, + ); + int statusCode = response.response.statusCode ?? 0; + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + return false; + } + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/auth/services/reset_password_service.dart b/lib/features/auth/services/reset_password_service.dart new file mode 100644 index 00000000..9a7a101d --- /dev/null +++ b/lib/features/auth/services/reset_password_service.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'reset_password_service.g.dart'; + +@RestApi() +abstract class ResetPasswordService { + factory ResetPasswordService(Dio dio, {String baseUrl}) = _ResetPasswordService; + + @POST('/api/send_password_reset_code') + Future sendEmailCodeForPasswordChange({ + @Body() required Map data, + }); + + @POST('/api/confirm_password_reset_code') + Future confirmPasswordChange({ + @Body() required Map data, + }); +} diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index f1dc435f..abb6537d 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; diff --git a/lib/features/settings/repositories/service_repository.dart b/lib/features/settings/repositories/service_repository.dart index e1e71f09..eb462de3 100644 --- a/lib/features/settings/repositories/service_repository.dart +++ b/lib/features/settings/repositories/service_repository.dart @@ -38,36 +38,4 @@ class SettingsRepository { rethrow; } } - - Future sendEmailCodeForPasswordChange(String email) async { - try { - return await settingsService.sendEmailCodeForPasswordChange( - data: { - 'email': email, - }, - ); - } catch (e) { - rethrow; - } - } - - Future confirmPasswordChange( - String code, - String password, - String passwordConfirmation, - String email, - ) async { - try { - return await settingsService.confirmPasswordChange( - data: { - 'code': code, - 'password': password, - 'password_confirmation': passwordConfirmation, - 'email': email, - }, - ); - } catch (e) { - rethrow; - } - } } diff --git a/lib/features/settings/services/settings_service.dart b/lib/features/settings/services/settings_service.dart index e4cdb6b7..d17b5842 100644 --- a/lib/features/settings/services/settings_service.dart +++ b/lib/features/settings/services/settings_service.dart @@ -21,14 +21,4 @@ abstract class SettingsService { Future updateProfileDetails({ @Body() required Map data, }); - - @POST('/api/send_password_reset_code') - Future sendEmailCodeForPasswordChange({ - @Body() required Map data, - }); - - @POST('/api/confirm_password_reset_code') - Future confirmPasswordChange({ - @Body() required Map data, - }); } diff --git a/pubspec.yaml b/pubspec.yaml index 6fe00c91..d39798fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: flutter_form_builder: ^10.0.1 image_picker: ^1.1.2 flutter_typeahead: ^5.2.0 - go_router: ^15.1.2 + go_router: ^15.2.3 keyboard_dismisser: ^3.0.0 retrofit: ^4.4.2 get_it: ^8.0.3 From 2a705fdb665e63ba6b50a316a921f7854dc11e2b Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 20 Jun 2025 15:45:50 +0300 Subject: [PATCH 06/13] feat(testflight): update version --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b0593717..4909ae60 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d93b6652..a05cd62d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 28 + 29 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index d39798fa..598e1121 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.5+35 +version: 1.1.6+36 publish_to: 'none' environment: From 85d67ba9c481b4d11cc96aa8190166a27a64366a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Sun, 22 Jun 2025 18:27:25 +0300 Subject: [PATCH 07/13] fix(chat): change sendChatMessage method --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- lib/core/app_router.dart | 6 ++ .../auth/presentation/pages/auth_screen.dart | 92 ++++++++++++------- .../pages/scan_qr_code_screen.dart | 20 ++++ .../bloc/chat_room_cubit/chat_room_cubit.dart | 14 ++- .../chat/presentation/pages/chat_screen.dart | 14 ++- .../chat/repositories/chat_repository.dart | 5 +- pubspec.yaml | 2 +- 9 files changed, 116 insertions(+), 45 deletions(-) create mode 100644 lib/features/auth/presentation/pages/scan_qr_code_screen.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4909ae60..4fc6eec0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 29; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 29; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 29; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index a05cd62d..f93e2e21 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 29 + 30 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index a07e12ba..c29754ab 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -96,6 +97,11 @@ class AppRouter { name: 'forgetPasswordScreen', builder: (context, state) => const ForgetPasswordScreen(), ), + GoRoute( + path: 'scan-qr-code-screen', + name: 'scanQRCodeScreen', + builder: (context, state) => const ScanQRCodeScreen(), + ), ], ), ShellRoute( diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 123eac0f..147e71cd 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -204,38 +204,68 @@ class _AuthScreenState extends State { ), ), const SizedBox(height: 16), - SizedBox( - height: 48, - width: MediaQuery.of(context).size.width - 32, - child: FloatingActionButton.extended( - heroTag: 'authButton', - highlightElevation: 0, - elevation: 0, - focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + Row( + children: [ + SizedBox( + height: 48, + width: MediaQuery.of(context).size.width - 88, + child: FloatingActionButton.extended( + heroTag: 'authButton', + highlightElevation: 0, + elevation: 0, + focusElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, + onPressed: state is AuthStateLoading + ? null + : () async { + final isAuthenticated = await authCubit.getTokenByCredentials( + emailController.text, + passwordController.text, + ); + if (isAuthenticated) { + if (context.mounted) { + _reconnectWebSocket(context); + } + } + }, + label: state is AuthStateLoading + ? const CupertinoActivityIndicator() + : Text( + 'Войти', + style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), + ), + ), ), - backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, - onPressed: state is AuthStateLoading - ? null - : () async { - final isAuthenticated = await authCubit.getTokenByCredentials( - emailController.text, - passwordController.text, - ); - if (isAuthenticated) { - if (context.mounted) { - _reconnectWebSocket(context); - } - } - }, - label: state is AuthStateLoading - ? const CupertinoActivityIndicator() - : Text( - 'Войти', - style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), - ), - ), + const SizedBox(width: 8), + SizedBox( + height: 48, + width: 48, + child: FloatingActionButton.extended( + heroTag: 'authButton', + highlightElevation: 0, + elevation: 0, + focusElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, + onPressed: state is AuthStateLoading + ? null + : () { + GoRouter.of(context).pushNamed('scanQRCodeScreen'); + }, + label: state is AuthStateLoading + ? const CupertinoActivityIndicator() + : const Icon( + Icons.qr_code_scanner_rounded, + color: Colors.white, + ), + ), + ), + ], ), ], ), diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart new file mode 100644 index 00000000..2b72d3a0 --- /dev/null +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class ScanQRCodeScreen extends StatefulWidget { + const ScanQRCodeScreen({super.key}); + + @override + State createState() => _ScanQRCodeScreenState(); +} + +class _ScanQRCodeScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Сканирование QR-кода'), + centerTitle: true, + ), + ); + } +} diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 58a53ac0..cbb886c2 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -176,9 +176,19 @@ class ChatRoomCubit extends Cubit { }); } - Future sendMessage({required String chatId, String? data, List? files}) async { + Future sendMessage({ + required String chatId, + String? data, + List? files, + required String userSlug, + }) async { try { - await chatRepository.sendMessage(chatId: chatId, data: data, files: files != null ? await ImageCompressUtility.compressFilesList(files) : null); + await chatRepository.sendMessage( + chatId: chatId, + data: data, + files: files != null ? await ImageCompressUtility.compressFilesList(files) : null, + slug: userSlug, + ); AudioPlayer().play((AssetSource('audio/send_message.wav'))); return true; } catch (e, st) { diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 3f7f1cdd..ba08452a 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -341,7 +341,7 @@ class _ChatScreenState extends State { children: [ const SizedBox(width: 8), InkWell( - onTap: () => showImagePicker(), + onTap: () => showImagePicker(state.chatRoom.users.receiver.slug), customBorder: const CircleBorder(), child: Ink( height: 40, @@ -386,7 +386,11 @@ class _ChatScreenState extends State { customBorder: const CircleBorder(), onTap: () { if (textEditingController.text.isNotEmpty) { - chatRoomCubit.sendMessage(chatId: widget.chatId, data: textEditingController.text.trim()); + chatRoomCubit.sendMessage( + chatId: widget.chatId, + data: textEditingController.text.trim(), + userSlug: state.chatRoom.users.receiver.slug, + ); setState(() { textEditingController.clear(); }); @@ -421,7 +425,7 @@ class _ChatScreenState extends State { ); } - showImagePicker() { + showImagePicker(String slug) { showCupertinoModalPopup( context: context, builder: (context) => CupertinoActionSheet( @@ -432,7 +436,7 @@ class _ChatScreenState extends State { final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.camera); if (image != null) { - await chatRoomCubit.sendMessage(chatId: widget.chatId, files: [File(image.path)]); + await chatRoomCubit.sendMessage(chatId: widget.chatId, files: [File(image.path)], userSlug: slug); } }, child: const Row( @@ -472,7 +476,7 @@ class _ChatScreenState extends State { ); if (result != null) { List files = result.paths.map((path) => File(path!)).toList(); - await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files); + await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files, userSlug: slug); } }, child: const Row( diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 4d1e9214..1215a5f6 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -28,6 +28,7 @@ class ChatRepository { Future sendMessage({ required String chatId, + required String slug, String? data, List? files, }) async { @@ -46,8 +47,8 @@ class ChatRepository { } } - await chatService.sendMessage( - id: chatId, + await chatService.sendMessageByUserSlug( + userSlug: slug, data: data, mediaData: multipartFiles.isEmpty ? null : multipartFiles, ); diff --git a/pubspec.yaml b/pubspec.yaml index 598e1121..e9316685 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.6+36 +version: 1.1.7+37 publish_to: 'none' environment: From 75fa9ebc5c09a40e397f02b77780a7ca2dffb706 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 23 Jun 2025 19:46:23 +0300 Subject: [PATCH 08/13] feat(chat): fix chat first message, add pick images from gallery --- lib/core/app_router.dart | 7 +- lib/core/websockets/ws_manager.dart | 3 +- .../bloc/chat_room_cubit/chat_room_cubit.dart | 2 - .../chat/presentation/pages/chat_screen.dart | 40 ++- .../presentation/pages/empty_chat_screen.dart | 289 ++++++++++++++++++ .../chat/repositories/chat_repository.dart | 9 - .../bloc/profile_cubit/profile_cubit.dart | 13 +- .../presentation/pages/profile_screen.dart | 2 +- .../widgets/sub_action_button_widget.dart | 14 +- .../presentation/widgets/sub_widget.dart | 2 + 10 files changed, 347 insertions(+), 34 deletions(-) create mode 100644 lib/features/chat/presentation/pages/empty_chat_screen.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index c29754ab..2affcfd1 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -17,6 +17,7 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/b import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; @@ -173,7 +174,6 @@ class AppRouter { name: 'welcomeScreen', builder: (context, state) => const WelcomeScreen(), redirect: (context, state) async { - try { if (await TokenStorage.haveTokenInStorage()) { final firebaseApi = sl(); @@ -312,6 +312,11 @@ class AppRouter { ); }, ), + GoRoute( + path: '/empty-chat-screen', + name: 'emptyChatScreen', + builder: (context, state) => EmptyChatScreen(author: state.extra as Author), + ), GoRoute( path: '/settings-screen', name: 'settingsScreen', diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 02857ca0..152f0fb9 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -39,6 +39,7 @@ class WsManager { metadata: const PusherChannelsOptionsMetadata.byDefault(), port: 443, ); + client = PusherChannelsClient.websocket( options: options, connectionErrorHandler: (exception, trace, refresh) { @@ -136,7 +137,7 @@ class WsManager { _privateDirectChatChannel?.unsubscribe(); } - Future clear() async{ + Future clear() async { await client?.disconnect(); client = null; userId = null; diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index cbb886c2..369993e0 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -177,14 +177,12 @@ class ChatRoomCubit extends Cubit { } Future sendMessage({ - required String chatId, String? data, List? files, required String userSlug, }) async { try { await chatRepository.sendMessage( - chatId: chatId, data: data, files: files != null ? await ImageCompressUtility.compressFilesList(files) : null, slug: userSlug, diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index ba08452a..e2e222cd 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -387,7 +387,6 @@ class _ChatScreenState extends State { onTap: () { if (textEditingController.text.isNotEmpty) { chatRoomCubit.sendMessage( - chatId: widget.chatId, data: textEditingController.text.trim(), userSlug: state.chatRoom.users.receiver.slug, ); @@ -411,14 +410,15 @@ class _ChatScreenState extends State { ), if (showAboutOrderButton) Positioned( - top: 8, - right: 15, - left: 16, - child: AboutOrderButton( - onTap: () async { - GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id}); - }, - )), + top: 8, + right: 15, + left: 16, + child: AboutOrderButton( + onTap: () async { + GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id}); + }, + ), + ), ], ), ), @@ -436,7 +436,7 @@ class _ChatScreenState extends State { final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.camera); if (image != null) { - await chatRoomCubit.sendMessage(chatId: widget.chatId, files: [File(image.path)], userSlug: slug); + await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); } }, child: const Row( @@ -447,6 +447,24 @@ class _ChatScreenState extends State { ], ), ), + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + final picker = ImagePicker(); + List images = await picker.pickMultiImage(); + if (images != []) { + List files = images.map((image) => File(image.path)).toList(); + await chatRoomCubit.sendMessage(files: files, userSlug: slug); + } + }, + child: const Row( + children: [ + Icon(Icons.photo_library_rounded), + SizedBox(width: 8), + Text('Галерея'), + ], + ), + ), CupertinoActionSheetAction( onPressed: () async { Navigator.of(context).pop(); @@ -476,7 +494,7 @@ class _ChatScreenState extends State { ); if (result != null) { List files = result.paths.map((path) => File(path!)).toList(); - await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files, userSlug: slug); + await chatRoomCubit.sendMessage(files: files, userSlug: slug); } }, child: const Row( diff --git a/lib/features/chat/presentation/pages/empty_chat_screen.dart b/lib/features/chat/presentation/pages/empty_chat_screen.dart new file mode 100644 index 00000000..2f8e91b3 --- /dev/null +++ b/lib/features/chat/presentation/pages/empty_chat_screen.dart @@ -0,0 +1,289 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; + +class EmptyChatScreen extends StatefulWidget { + final Author author; + + const EmptyChatScreen({ + super.key, + required this.author, + }); + + @override + State createState() => _EmptyChatScreenState(); +} + +class _EmptyChatScreenState extends State { + final TextEditingController textEditingController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + titleSpacing: 0, + backgroundColor: appTheme.gray100, + centerTitle: false, + title: GestureDetector( + onTap: () { + GoRouter.of(context).pushNamed( + 'profileScreen', + extra: Author( + authorSlug: widget.author.authorSlug, + authorName: widget.author.authorName, + authorProfileIcon: widget.author.authorProfileIcon, + authorIsMe: false, + ), + ); + }, + child: Row( + children: [ + CircleAvatar( + radius: 20, + foregroundImage: NetworkImage(widget.author.authorProfileIcon), + backgroundImage: const AssetImage('assets/images/no_avatar.png'), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.author.authorName, + style: CustomTextStyles.titleSmallBold.copyWith( + color: PrimaryColors().black, + ), + ), + ], + ), + ], + ), + ), + ), + body: SafeArea( + child: Column( + children: [ + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const SizedBox(width: 8), + InkWell( + onTap: () => showImagePicker(widget.author.authorSlug), + customBorder: const CircleBorder(), + child: Ink( + height: 40, + width: 40, + child: Transform.rotate( + angle: pi / 4, + child: const Icon( + Icons.attach_file_rounded, + size: 30, + color: Color(0xFF858E99), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 40, + child: TextFormField( + expands: true, + textCapitalization: TextCapitalization.sentences, + controller: textEditingController, + maxLines: null, + minLines: null, + keyboardType: TextInputType.multiline, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + hintText: 'Сообщение', + hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)), + filled: true, + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + InkWell( + customBorder: const CircleBorder(), + onTap: () async { + if (textEditingController.text.isNotEmpty) { + await sl().sendMessage( + data: textEditingController.text.trim(), + slug: widget.author.authorSlug, + ); + setState(() { + textEditingController.clear(); + }); + final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'chatId': chatIdOrError, + }, + ); + } + }, + child: Ink( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + height: 40, + width: 40, + child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white), + ), + ), + const SizedBox(width: 8), + ], + ), + ), + ], + ), + ), + ); + } + + showImagePicker(String slug) { + showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + final picker = ImagePicker(); + XFile? image = await picker.pickImage(source: ImageSource.camera); + if (image != null) { + await sl().sendMessage( + files: [File(image.path)], + slug: widget.author.authorSlug, + ); + final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'chatId': chatIdOrError, + }, + ); + } + }, + child: const Row( + children: [ + Icon(Icons.photo_library_rounded), + SizedBox(width: 8), + Text('Галерея'), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + final picker = ImagePicker(); + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + List files = images.map((image) => File(image.path)).toList(); + await sl().sendMessage( + files: files, + slug: widget.author.authorSlug, + ); + final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'chatId': chatIdOrError, + }, + ); + } + }, + child: const Row( + children: [ + Icon(Icons.photo_camera), + SizedBox(width: 8), + Text('Фото'), + ], + ), + ), + CupertinoActionSheetAction( + onPressed: () async { + Navigator.of(context).pop(); + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: [ + 'pdf', + 'doc', + 'docx', + 'json', + 'ppt', + 'pptx', + 'rtf', + 'txt', + 'xls', + 'xlsx', + 'csv', + 'jpeg', + 'jpg', + 'png', + 'gif', + 'svg', + 'xml', + 'webp', + ], + ); + if (result != null) { + List files = result.paths.map((path) => File(path!)).toList(); + await sl().sendMessage( + files: files, + slug: widget.author.authorSlug, + ); + final chatIdOrError = await sl().getChatByUserSlug(widget.author.authorSlug); + AppRouter.navigatorKey.currentContext?.pushReplacementNamed( + 'chatScreen', + extra: { + 'chatId': chatIdOrError, + }, + ); + } + }, + child: const Row( + children: [ + Icon(Icons.file_present_rounded), + SizedBox(width: 8), + Text('Файлы'), + ], + ), + ), + ], + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + isDestructiveAction: true, + child: const Text('Отмена'), + ), + ), + ); + } +} diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 1215a5f6..e249c708 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -27,7 +27,6 @@ class ChatRepository { } Future sendMessage({ - required String chatId, required String slug, String? data, List? files, @@ -168,12 +167,4 @@ class ChatRepository { return 'error'; } } - - Future sendFirstChatMessageForCreateChatRoom(String userSlug) async { - try { - return await chatService.sendMessageByUserSlug(userSlug: userSlug, data: 'Создал чат!'); - } catch (e) { - rethrow; - } - } } diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 530bc9aa..1312dea8 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; @@ -107,18 +108,14 @@ class ProfileCubit extends Cubit { } getChatRoomIdBySlug({ - required String slug, + required Author author, }) async { try { - final chatIdOrError = await chatRepository.getChatByUserSlug(slug); + final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug); if (chatIdOrError == '404') { - await chatRepository.sendFirstChatMessageForCreateChatRoom(slug); - final chatIdOrError = await chatRepository.getChatByUserSlug(slug); AppRouter.navigatorKey.currentContext?.pushNamed( - 'chatScreen', - extra: { - 'chatId': chatIdOrError, - }, + 'emptyChatScreen', + extra: author, ); } else if (chatIdOrError == 'error') { showErrorToast('Ошибка создания чата'); diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index a847e697..9dc4f880 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1232,7 +1232,7 @@ class _ProfileScreenState extends State { children: [ const SizedBox(width: 16), GestureDetector( - onTap: () async => await profileCubit.getChatRoomIdBySlug(slug: widget.author!.authorSlug), + onTap: () async => await profileCubit.getChatRoomIdBySlug(author: widget.author!), child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(8), diff --git a/lib/features/profile/presentation/widgets/sub_action_button_widget.dart b/lib/features/profile/presentation/widgets/sub_action_button_widget.dart index c1929a13..bca5e3da 100644 --- a/lib/features/profile/presentation/widgets/sub_action_button_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_action_button_widget.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; import 'package:flutter/material.dart'; @@ -11,11 +12,15 @@ enum SubActionType { sendMessage, subscribe } class SubActionButtonWidget extends StatefulWidget { final SubActionType subActionType; final String slug; + final String name; + final String porfileIcon; const SubActionButtonWidget({ super.key, required this.subActionType, required this.slug, + required this.name, + required this.porfileIcon, }); @override @@ -64,7 +69,14 @@ class _SubActionButtonWidgetState extends State { GoRouter.of(context).pushNamed( 'chatScreen', extra: { - 'chatId': await sl().getChatRoomIdBySlug(slug: widget.slug), + 'chatId': await sl().getChatRoomIdBySlug( + author: Author( + authorSlug: widget.slug, + authorName: widget.name, + authorProfileIcon: widget.porfileIcon, + authorIsMe: false, + ), + ), }, ); }, diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index aa17433d..27365fd2 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -70,6 +70,8 @@ class _SubWidgetState extends State { SubActionButtonWidget( subActionType: widget.subModel.isEntitySubscribedByMe ? SubActionType.sendMessage : SubActionType.subscribe, slug: widget.subModel.slug, + name: widget.subModel.name, + porfileIcon: widget.subModel.profilePhotoUrl, ), ], ), From 7530270a2e909a7fae468ced1cf3aab4308aaee2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 25 Jun 2025 12:22:14 +0300 Subject: [PATCH 09/13] feat(qr+devtools): scan qr code and dev tools --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- .../auth/presentation/pages/auth_screen.dart | 6 +- .../pages/scan_qr_code_screen.dart | 70 ++++++++++ .../widgets/bottom_navigation_bar_widget.dart | 21 ++- .../presentation/dev_tools_screen.dart | 124 ++++++++++-------- pubspec.yaml | 3 +- 7 files changed, 165 insertions(+), 67 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4fc6eec0..e6a8ab5e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 30; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f93e2e21..71ddffdc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 30 + 31 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 147e71cd..877aaf1a 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -88,10 +88,10 @@ class _AuthScreenState extends State { title: GestureDetector( onTap: () { tapsCount++; - if (tapsCount == 6) { + if (tapsCount == 7) { tapsCount = 0; + GoRouter.of(context).pushNamed(DevToolsScreen.name); } - GoRouter.of(context).pushNamed(DevToolsScreen.name); }, child: SvgPicture.asset( ImageConstant.imgLogoEatvio, @@ -244,7 +244,7 @@ class _AuthScreenState extends State { height: 48, width: 48, child: FloatingActionButton.extended( - heroTag: 'authButton', + heroTag: 'scanQrCodeButton', highlightElevation: 0, elevation: 0, focusElevation: 0, diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 2b72d3a0..35f56b50 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -1,4 +1,14 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; class ScanQRCodeScreen extends StatefulWidget { const ScanQRCodeScreen({super.key}); @@ -8,6 +18,20 @@ class ScanQRCodeScreen extends StatefulWidget { } class _ScanQRCodeScreenState extends State { + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + Barcode? result; + QRViewController? controller; + + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } else if (Platform.isIOS) { + controller!.resumeCamera(); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -15,6 +39,52 @@ class _ScanQRCodeScreenState extends State { title: const Text('Сканирование QR-кода'), centerTitle: true, ), + body: Column( + children: [ + Expanded( + child: QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, + ), + ), + ], + ), ); } + + void _onQRViewCreated(QRViewController controller) { + this.controller = controller; + controller.scannedDataStream.listen((scanData) async { + setState(() { + result = scanData; + }); + controller.pauseCamera(); + try { + final scannedUrl = scanData.code; + final uri = Uri.parse(scannedUrl ?? ''); + final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : ''; + baseUrl = scannedBaseUrl; + sl().options.baseUrl = baseUrl; + final response = await Dio().getUri( + uri, + options: Options( + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ), + ); + final token = response.data['token']; + await TokenStorage.saveTokenInStorage(token); + AppRouter.navigatorKey.currentContext?.go( + '/feed-screen', + extra: {'needToInitializeWebSocket': false}, + ); + showSuccessToast('Добро пожаловать!'); + } catch (e) { + controller.resumeCamera(); + showErrorToast('Что-то пошло не так, попробуйте обновить страницу с qr кодом или используйте вход через логин и пароль'); + } + }); + } } diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index e370452a..a67fbbaf 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -1,6 +1,7 @@ import 'package:badges/badges.dart' as badges; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -30,6 +31,7 @@ class BottomNavigationBarWidget extends StatefulWidget { class _BottomNavigationBarWidgetState extends State { int currentBottomNavBarIndex = 1; late final List pages; + int tapsCount = 0; @override void initState() { @@ -58,12 +60,29 @@ class _BottomNavigationBarWidgetState extends State { } void _onItemTapped(int index) async { + if (pages.length == 7) { + if (index == 6) { + tapsCount++; + if (tapsCount == 7) { + tapsCount = 0; + GoRouter.of(context).pushNamed(DevToolsScreen.name); + } + } + } else { + if (index == 5) { + tapsCount++; + if (tapsCount == 7) { + tapsCount = 0; + GoRouter.of(context).pushNamed(DevToolsScreen.name); + } + } + } if (currentBottomNavBarIndex != index) { setState(() { currentBottomNavBarIndex = index; }); + GoRouter.of(context).replace(pages[currentBottomNavBarIndex]); } - GoRouter.of(context).replace(pages[currentBottomNavBarIndex]); } @override diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index 4658b10a..a57e7580 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -81,6 +81,8 @@ class _DevToolsScreenContentState extends State { onPressed: () { setState(() { baseUrlForTest = controller.text; + baseUrl = baseUrlForTest.toLowerCase(); + sl().options.baseUrl = baseUrl; controller.clear(); }); }, @@ -89,73 +91,79 @@ class _DevToolsScreenContentState extends State { ], ), const SizedBox(height: 10), - if (baseUrlForTest != baseUrl) - CustomOutlinedButton( - text: 'Set dev2 base url', - onPressed: () { - setState(() { - baseUrlForTest = baseUrl; - }); - }, - buttonStyle: const ButtonStyle(), - ), + CustomOutlinedButton( + text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url', + onPressed: () { + setState(() { + if (baseUrl == 'https://eatvio.ru') { + baseUrlForTest = 'https://dev2.eatvio.com'; + } else { + baseUrlForTest = 'https://eatvio.ru'; + } + baseUrl = baseUrlForTest.toLowerCase(); + sl().options.baseUrl = baseUrl; + controller.clear(); + }); + }, + buttonStyle: const ButtonStyle(), + ), SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - ListView.builder( - shrinkWrap: true, - itemCount: testCases.length, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: - (context, index) => Padding( - padding: const EdgeInsets.only(bottom: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: Text('${index + 1}. ${testCases[index].name}')), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - CustomOutlinedButton( - text: 'run', - height: 20, - width: 60, - onPressed: () async { - if (testsStates[index] == TestState.loading) return; + if (baseUrl.toLowerCase() != 'https://eatvio.ru') + ListView.builder( + shrinkWrap: true, + itemCount: testCases.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: Text('${index + 1}. ${testCases[index].name}')), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomOutlinedButton( + text: 'run', + height: 20, + width: 60, + onPressed: () async { + if (testsStates[index] == TestState.loading) return; + setState(() { + testsStates[index] = TestState.loading; + }); + final result = await testCases[index].run(baseUrl: baseUrlForTest); + if (result) { setState(() { - testsStates[index] = TestState.loading; + testsStates[index] = TestState.success; }); - final result = await testCases[index].run(baseUrl: baseUrlForTest); - if (result) { - setState(() { - testsStates[index] = TestState.success; - }); - showSuccessToast('${testCases[index].name} passed'); - } else { - setState(() { - testsStates[index] = TestState.fail; - }); - showErrorToast('${testCases[index].name} failed'); - } - }, - buttonStyle: const ButtonStyle(), - ), - const SizedBox(width: 5), - switch (testsStates[index]) { - TestState.success => Container(width: 20, height: 20, color: Colors.green), - TestState.fail => Container(width: 20, height: 20, color: Colors.red), - TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), - TestState.loading => const SizedBox( + showSuccessToast('${testCases[index].name} passed'); + } else { + setState(() { + testsStates[index] = TestState.fail; + }); + showErrorToast('${testCases[index].name} failed'); + } + }, + buttonStyle: const ButtonStyle(), + ), + const SizedBox(width: 5), + switch (testsStates[index]) { + TestState.success => Container(width: 20, height: 20, color: Colors.green), + TestState.fail => Container(width: 20, height: 20, color: Colors.red), + TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), + TestState.loading => const SizedBox( width: 20, height: 20, child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), ), - }, - ], - ), - ], - ), + }, + ], + ), + ], ), - ), + ), + ), const SizedBox(height: 10), CustomOutlinedButton( text: 'add token to server', diff --git a/pubspec.yaml b/pubspec.yaml index e9316685..9e4ef5af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.7+37 +version: 1.1.8+38 publish_to: 'none' environment: @@ -71,6 +71,7 @@ dependencies: package_info_plus: ^8.3.0 device_info_plus: ^11.4.0 permission_handler: ^12.0.0+1 + qr_code_scanner_plus: ^2.0.10+1 dev_dependencies: build_runner: ^2.4.15 From 55bc361b054a767e0d3d77d411403878b800d400 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 25 Jun 2025 14:47:49 +0300 Subject: [PATCH 10/13] fix(qr+menu): text --- lib/features/auth/presentation/pages/scan_qr_code_screen.dart | 2 +- lib/features/menu/presentation/pages/menu_screen.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 35f56b50..b266904a 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -83,7 +83,7 @@ class _ScanQRCodeScreenState extends State { showSuccessToast('Добро пожаловать!'); } catch (e) { controller.resumeCamera(); - showErrorToast('Что-то пошло не так, попробуйте обновить страницу с qr кодом или используйте вход через логин и пароль'); + showErrorToast('Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.'); } }); } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index f657c6a7..c8fa2135 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -114,7 +114,7 @@ class MenuScreen extends StatelessWidget { child: const Text("Отмена"), ), ], - content: const Text("Вы уверены что хотите выйти из аккаунта?"), + content: const Text("Вы уверены, что хотите выйти из аккаунта?"), ), ); }; From 9c168d37ce5468ce7df11b4fea2783f9c8ff668f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 25 Jun 2025 14:56:59 +0300 Subject: [PATCH 11/13] fix: black screen --- .../bloc/profile_avatar_cubit/profile_avatar_cubit.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index ae45a6ca..94ae4601 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -1,6 +1,8 @@ +import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class ProfileAvatarCubit extends Cubit { final ProfileRepository profileRepository; @@ -18,6 +20,7 @@ class ProfileAvatarCubit extends Cubit { ), ); } catch (e) { + AppRouter.navigatorKey.currentContext?.go('/auth-screen'); emit(ProfileAvatarState.error(errorMessage: e.toString())); } } From 8c7c7f51cf86bc8483615e23e9c583bb1d960b6f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 25 Jun 2025 15:32:37 +0300 Subject: [PATCH 12/13] fix: auth --- .../auth/presentation/pages/auth_screen.dart | 16 ++++++++----- .../pages/scan_qr_code_screen.dart | 24 +++++++++++++++++-- lib/main.dart | 2 +- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 877aaf1a..31028059 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -60,17 +61,20 @@ class _AuthScreenState extends State { } if (context.mounted) { try { - context.read().getMyProfile(); - context.read().getChatRoomsWithLoading(); - context.read().connectToWebSocket(); - context.read().getNotifications(); - context.read().connectToWebSocket(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); showSuccessToast('Успешный вход'); } catch (e) { rethrow; } - GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } + AppRouter.navigatorKey.currentContext?.go( + '/feed-screen', + extra: {'needToInitializeWebSocket': false}, + ); } @override diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index b266904a..2ea99f06 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -6,7 +6,12 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; @@ -58,7 +63,7 @@ class _ScanQRCodeScreenState extends State { setState(() { result = scanData; }); - controller.pauseCamera(); + await controller.pauseCamera(); try { final scannedUrl = scanData.code; final uri = Uri.parse(scannedUrl ?? ''); @@ -76,13 +81,28 @@ class _ScanQRCodeScreenState extends State { ); final token = response.data['token']; await TokenStorage.saveTokenInStorage(token); + try { + await sl().clear(); + await sl().initialize(); + } catch (e) { + rethrow; + } + try { + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + } catch (e) { + rethrow; + } AppRouter.navigatorKey.currentContext?.go( '/feed-screen', extra: {'needToInitializeWebSocket': false}, ); showSuccessToast('Добро пожаловать!'); } catch (e) { - controller.resumeCamera(); + await controller.resumeCamera(); showErrorToast('Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.'); } }); diff --git a/lib/main.dart b/lib/main.dart index a712f6e3..005afaac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,7 @@ Future main() async { await di.init(); await SentryFlutter.init( - (options) { + (options) { options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; options.tracesSampleRate = 1.0; options.debug = false; From 80afcd89498035750710bcd49ef9da8220c85c6e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 25 Jun 2025 15:36:27 +0300 Subject: [PATCH 13/13] upd ver to 32 --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- .../auth/presentation/pages/scan_qr_code_screen.dart | 1 - pubspec.yaml | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e6a8ab5e..d38fc84e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 71ddffdc..83272a6a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 31 + 32 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 2ea99f06..411cb3a5 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -11,7 +11,6 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/prof import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 9e4ef5af..dc39773a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.1.8+38 +version: 1.1.9+39 publish_to: 'none' environment: