diff --git a/.gitignore b/.gitignore index 4f05e3a1..06e89a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ *.mocks.dart android/build/ ios/XCBuildData/ -env/.env \ No newline at end of file +env/.env +.env \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 198e6f7b..1fee690d 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 = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -410,6 +410,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -538,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -560,6 +561,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -576,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -598,6 +600,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b05e5fac..f0f66aff 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 72 + 73 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 971ebb2c..37713f29 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/core/views/service/views_service.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/auth_cubit.dart'; @@ -137,7 +140,7 @@ Future init() async { sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); - sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); + sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory( () => DiaryCubit( @@ -179,6 +182,7 @@ Future init() async { sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -212,6 +216,7 @@ Future init() async { sl.registerLazySingleton(() => CommentsRepository(service: sl())); sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); + sl.registerFactory(() => ViewsRepository(viewsService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -247,6 +252,7 @@ Future init() async { sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => ConsultantRatingService(sl())); sl.registerLazySingleton(() => NotificationSettingsService(sl())); + sl.registerLazySingleton(() => ViewsService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/views/cubit/views_cubit.dart b/lib/core/views/cubit/views_cubit.dart new file mode 100644 index 00000000..ee15af22 --- /dev/null +++ b/lib/core/views/cubit/views_cubit.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'package:eatvio_app/core/views/cubit/views_state.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ViewsCubit extends Cubit { + final ViewsRepository viewsRepository; + ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) { + _listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews); + } + + late final AppLifecycleListener _listener; + + final Set postsIds = {}; + final Set plansIds = {}; + final Set dietsIds = {}; + final Set recipesIds = {}; + + final Set _sentPosts = {}; + final Set _sentPlans = {}; + final Set _sentDiets = {}; + final Set _sentRecipes = {}; + + final Map> _visibilityTimers = { + ViewableContentType.posts: {}, + ViewableContentType.plans: {}, + ViewableContentType.diets: {}, + ViewableContentType.recipes: {}, + }; + + Timer? _debounceTimer; + + Set _idsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return postsIds; + case ViewableContentType.plans: + return plansIds; + case ViewableContentType.diets: + return dietsIds; + case ViewableContentType.recipes: + return recipesIds; + } + } + + Set _sentIdsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return _sentPosts; + case ViewableContentType.plans: + return _sentPlans; + case ViewableContentType.diets: + return _sentDiets; + case ViewableContentType.recipes: + return _sentRecipes; + } + } + + void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { + final timers = _visibilityTimers[type]!; + final ids = _idsForType(type); + final sent = _sentIdsForType(type); + + if (sent.contains(id)) { + timers[id]?.cancel(); + timers.remove(id); + return; + } + + if (ids.contains(id)) return; + + if (visibleFraction >= 0.5) { + if (timers.containsKey(id)) return; + timers[id] = Timer(const Duration(seconds: 1), () { + ids.add(id); + timers.remove(id); + _scheduleDebouncedSend(); + }); + } else { + timers[id]?.cancel(); + timers.remove(id); + } + } + + Future _viewContent({required ViewableContentType type, required List ids}) async { + await viewsRepository.viewContent( + viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids), + ); + } + + void _scheduleDebouncedSend() { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(seconds: 4), _sendViews); + } + + void _send(Set ids, Set sent, ViewableContentType type) { + final newIds = ids.difference(sent); + if (newIds.isNotEmpty) { + _viewContent(type: type, ids: newIds.toList()); + sent.addAll(newIds); + ids.removeAll(newIds); + } + } + + void _sendViews() { + _send(postsIds, _sentPosts, ViewableContentType.posts); + _send(plansIds, _sentPlans, ViewableContentType.plans); + _send(dietsIds, _sentDiets, ViewableContentType.diets); + _send(recipesIds, _sentRecipes, ViewableContentType.recipes); + } + + @override + Future close() async { + for (final map in _visibilityTimers.values) { + for (final t in map.values) { + t.cancel(); + } + map.clear(); + } + _debounceTimer?.cancel(); + _listener.dispose(); + _sendViews(); + await super.close(); + } +} diff --git a/lib/core/views/cubit/views_state.dart b/lib/core/views/cubit/views_state.dart new file mode 100644 index 00000000..e43d3ff5 --- /dev/null +++ b/lib/core/views/cubit/views_state.dart @@ -0,0 +1,3 @@ +class ViewsCubitState { + ViewsCubitState(); +} diff --git a/lib/core/views/repository/views_repository.dart b/lib/core/views/repository/views_repository.dart new file mode 100644 index 00000000..165a4956 --- /dev/null +++ b/lib/core/views/repository/views_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; + +class ViewsRepository { + final ViewsService viewsService; + + ViewsRepository({required this.viewsService}); + + Future viewContent({required ViewModel viewModel}) async { + try { + await viewsService.viewContent(viewModel: viewModel); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/core/views/service/views_service.dart b/lib/core/views/service/views_service.dart new file mode 100644 index 00000000..e89990fa --- /dev/null +++ b/lib/core/views/service/views_service.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'views_service.g.dart'; + +@RestApi() +abstract class ViewsService { + factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService; + + @POST('/api/views') + Future viewContent({ + @Body() required ViewModel viewModel, + }); +} diff --git a/lib/features/auth/models/social_exchange_request.dart b/lib/features/auth/models/social_exchange_request.dart new file mode 100644 index 00000000..c322fab6 --- /dev/null +++ b/lib/features/auth/models/social_exchange_request.dart @@ -0,0 +1,12 @@ +class SocialExchangeRequest { + final String provider; + final String accessToken; + + SocialExchangeRequest({required this.provider, required this.accessToken}); + + Map toJson() { + return {'provider': provider, 'access_token': accessToken}; + } +} + + diff --git a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 20686cce..7b58a4b0 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - class AuthCubit extends Cubit { final AuthRepository authRepository; - AuthCubit({ - required this.authRepository, - }) : super(const AuthState()); + AuthCubit({required this.authRepository}) : super(const AuthState()); Future getTokenByCredentials(String email, String password) async { emit(const AuthState.loading()); try { - final token = (await authRepository.getTokenByCredentials( - email, - password, - )) - .token; - emit( - AuthState.loaded( - token: token, - ), - ); - await TokenStorage.saveTokenInStorage(token); + final token = (await authRepository.getTokenByCredentials(email, password)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); return true; } catch (e) { log(e.toString()); @@ -36,4 +25,19 @@ class AuthCubit extends Cubit { return false; } } + + Future getTokenBySocial({required String provider, required String accessToken}) async { + emit(const AuthState.loading()); + try { + final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); + return true; + } catch (e) { + log(e.toString()); + showErrorToast('Не удалось войти через $provider'); + emit(AuthState.error(errorMessage: '$e')); + return false; + } + } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 73162bd2..8a4e711b 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -24,6 +24,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart'; class AuthScreen extends StatefulWidget { const AuthScreen({super.key}); @@ -165,6 +166,52 @@ class _AuthScreenState extends State { ), ], ), + // const SizedBox(height: 16), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // _SocialButton( + // onTap: _onVkPressed, + // backgroundColor: const Color(0xFF2787F5), + // tooltip: 'VK ID', + // child: SvgPicture.asset( + // 'assets/icons/vk.svg', + // height: 24, + // width: 24, + // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + // ), + // ), + // _SocialButton( + // onTap: _onOkPressed, + // backgroundColor: const Color(0xFFF58220), + // tooltip: 'Одноклассники', + // child: SvgPicture.asset( + // 'assets/icons/ok.svg', + // height: 24, + // width: 24, + // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + // ), + // ), + // const _SocialButton( + // onTap: _onMailRuPressed, + // backgroundColor: Color(0xFF005FF9), + // tooltip: 'Mail.ru', + // child: Text( + // 'Mail', + // style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + // ), + // ), + // const _SocialButton( + // onTap: _onYandexPressed, + // backgroundColor: Color(0xFFFFCC00), + // tooltip: 'Яндекс ID', + // child: Text( + // 'Yandex', + // style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + // ), + // ), + // ], + // ), const Spacer(), Center( child: RichText( @@ -317,3 +364,86 @@ class _AuthScreenState extends State { ); } } + +class _SocialButton extends StatelessWidget { + const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); + + final VoidCallback onTap; + final Widget child; + final Color backgroundColor; + final String tooltip; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Ink( + height: 48, + width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4, + decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)), + child: Center(child: child), + ), + ), + ); + } +} + +void _onVkPressed() async { + final accessToken = await SocialAuthFacade.signInWithVk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } +} + +void _onOkPressed() async { + final accessToken = await SocialAuthFacade.signInWithOk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onMailRuPressed() async { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onYandexPressed() async { + final accessToken = await SocialAuthFacade.signInWithYandex(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'yandex', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +Future _reconnectAfterAuth() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + try { + await sl().clear(); + await sl().initialize(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешный вход'); + } catch (_) {} + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); +} diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index d0bfcce5..84df92f6 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; class AuthRepository { final AuthService authService; @@ -22,4 +23,17 @@ class AuthRepository { rethrow; } } + + Future getTokenBySocial({ + required String provider, + required String accessToken, + }) async { + try { + return await authService.getTokenBySocial( + body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + ); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart new file mode 100644 index 00000000..62985d5d --- /dev/null +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +class SocialAuthFacade { + static Future signInWithVk() async { + // TODO: Integrate official VK ID SDK and return access token + return null; + } + + static Future signInWithOk() async { + // TODO: Integrate official OK SDK and return access token + return null; + } + + static Future signInWithMailRu() async { + // TODO: Integrate official Mail.ru SDK and return access token + return null; + } + + static Future signInWithYandex() async { + // TODO: Integrate official Yandex ID SDK and return access token + return null; + } +} + + diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 62256f8b..7dabb65b 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; part 'auth_service.g.dart'; @@ -13,4 +14,9 @@ abstract class AuthService { Future getTokenByCredentials({ @Body() required CredentialsModel credentials, }); + + @POST('/api/social/login') + Future getTokenBySocial({ + @Body() required SocialExchangeRequest body, + }); } diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 760a8783..3d870e8d 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -3,9 +3,12 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +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:flutter/material.dart'; @@ -284,6 +287,19 @@ class _ConsultationCardWidgetState extends State { ), ], ), + // Кнопка получения выплаты + if (widget.consultationsModel.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[ + const SizedBox(height: 16), + CustomOutlinedButton( + text: s.collectCharge, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () async { + GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.consultationsModel.id); + }, + ), + ], ], ), ), diff --git a/lib/features/diary/presentation/widgets/meal_summary_widget.dart b/lib/features/diary/presentation/widgets/meal_summary_widget.dart index dd7108e6..f4537b79 100644 --- a/lib/features/diary/presentation/widgets/meal_summary_widget.dart +++ b/lib/features/diary/presentation/widgets/meal_summary_widget.dart @@ -82,6 +82,11 @@ class MealSummaryWidget extends StatelessWidget { continue; } + // Для продуктов из диеты учитываем только съеденные (с галочкой) + if (portion.fromDiet && portion.portionState != PortionState.eatenFromDiet) { + continue; + } + final caloriesInfo = portion.productCaloriesInfo; if (caloriesInfo == null) { continue; diff --git a/lib/features/feed/models/view_model.dart b/lib/features/feed/models/view_model.dart new file mode 100644 index 00000000..f04022e9 --- /dev/null +++ b/lib/features/feed/models/view_model.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_model.freezed.dart'; +part 'view_model.g.dart'; + +@freezed +abstract class ViewModel with _$ViewModel { + + factory ViewModel({ + required String type, + List? ids, + List? slugs, + }) = _ViewModel; + + factory ViewModel.fromJson(Map json) => _$ViewModelFromJson(json); +} \ No newline at end of file diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index d3ba615a..947b9a5d 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -81,4 +81,5 @@ class FeedCubit extends Cubit { Future viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); + } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index ce29b884..844788fe 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,5 +1,6 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; @@ -66,12 +67,9 @@ class _FeedScreenContentState extends State { final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); - @override void initState() { - // Initialize chipsLabelList based on hidePlans setting chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; - consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } @@ -80,6 +78,8 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } + + @override void dispose() { blogPostsScrollController.dispose(); @@ -90,6 +90,8 @@ class _FeedScreenContentState extends State { super.dispose(); } + + @override Widget build(BuildContext context) { FlutterNativeSplash.remove(); @@ -228,10 +230,10 @@ class _FeedScreenContentState extends State { ]; }, body: switch (_selectedChipIndex) { - 0 => const BlogPosts(), - 1 => const Recipes(), - 2 => const Diets(), - 3 when !hidePlans => const Plans(), + 0 => BlogPosts(handleVisibility: context.read().handleVisibility), + 1 => Recipes(handleVisibility: context.read().handleVisibility), + 2 => Diets(handleVisibility: context.read().handleVisibility), + 3 when !hidePlans => Plans(handleVisibility: context.read().handleVisibility), 3 when hidePlans => const Consultants(), 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), @@ -240,3 +242,20 @@ class _FeedScreenContentState extends State { ); } } + +enum ViewableContentType { + plans, + posts, + diets, + recipes; + + String getStringifiedVersion() => switch (this) { + ViewableContentType.plans => 'plans', + + ViewableContentType.posts => 'blog_posts', + + ViewableContentType.diets => 'diets', + + ViewableContentType.recipes => 'recipes', + }; +} diff --git a/lib/features/feed/presentation/widgets/blog_posts.dart b/lib/features/feed/presentation/widgets/blog_posts.dart index 8c918515..b9da2ecc 100644 --- a/lib/features/feed/presentation/widgets/blog_posts.dart +++ b/lib/features/feed/presentation/widgets/blog_posts.dart @@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class BlogPosts extends StatelessWidget { - const BlogPosts({super.key}); - + const BlogPosts({super.key, required this.handleVisibility}); + final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility; + @override Widget build(BuildContext context) { return RefreshIndicator( @@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget { if (index == state.feedBlogPostList.length) { return const Loader(); } - return BlogPostWidget(index: index); + return VisibilityDetector( + onVisibilityChanged: (info) { + final id = state.feedBlogPostList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + key: ValueKey(index), + child: BlogPostWidget(index: index), + ); }, separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, ), ); } else if (state is FeedStateError) { - return DataErrorWidget( - onReload: () => context.read().loadFeedBlogPostList(), - ); + return DataErrorWidget(onReload: () => context.read().loadFeedBlogPostList()); } else { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } }, ), ); } } - diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index c08a82a6..080b1a14 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Diets extends StatelessWidget { - const Diets({super.key}); - + const Diets({super.key, required this.handleVisibility}); + final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -19,10 +21,7 @@ class Diets extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, DietsState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.errorMessage != null) { return DataErrorWidget(onReload: () => context.read().loadDiets()); } else { @@ -39,7 +38,14 @@ class Diets extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return DietsWidget(diet: state.dietsList[index], index: index); + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.dietsList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets); + }, + child: DietsWidget(diet: state.dietsList[index], index: index), + ); }, separatorBuilder: (context, index) { return const SizedBox(height: 8); @@ -50,11 +56,7 @@ class Diets extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: DietFilterBottomSheet( - dietsCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read())), ), ], ); diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 005c9ee9..42e107e5 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Plans extends StatelessWidget { - const Plans({super.key}); - + const Plans({super.key, required this.handleVisibility}); + final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -19,10 +21,7 @@ class Plans extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, PlansState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.errorMessage != null) { return DataErrorWidget(onReload: () => context.read().loadPlans()); } @@ -39,9 +38,13 @@ class Plans extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return PlanWidget( - plan: state.plans[index], - index: index, + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.plans[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans); + }, + child: PlanWidget(plan: state.plans[index], index: index), ); }, separatorBuilder: (context, index) { @@ -53,11 +56,7 @@ class Plans extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: PlansFilterBottomSheet( - plansCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read())), ), ], ); diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index 914c31fe..1271a493 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Recipes extends StatelessWidget { - const Recipes({super.key}); - + const Recipes({super.key, required this.handleVisibility}); + final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -40,9 +42,16 @@ class Recipes extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return RecipesWidget( - recipe: state.recipesList[index], - index: index, + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.recipesList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes); + }, + child: RecipesWidget( + recipe: state.recipesList[index], + index: index, + ), ); }, separatorBuilder: (context, index) { diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index e6e95969..108a8b11 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart'; class FeedRepository { final FeedService feedService; - FeedRepository({ - required this.feedService, - }); + FeedRepository({required this.feedService}); - Future getFeedBlogPosts({ - String? query, - int page = 1, - }) async { + Future getFeedBlogPosts({String? query, int page = 1}) async { try { - return (await feedService.getBlogPostList( - page: page, - query: query, - )); + return (await feedService.getBlogPostList(page: page, query: query)); } catch (e) { rethrow; } } - Future getBlogPost({ - required int id - }) async { + Future getBlogPost({required int id}) async { try { - return await feedService.getBlogPost( - postId: id - ); + return await feedService.getBlogPost(postId: id); } catch (e) { rethrow; } } - Future> getRecipes({ - String? query, - List? tags, - int page = 1, - }) async { + Future> getRecipes({String? query, List? tags, int page = 1}) async { try { - return (await feedService.getRecipes( - query: query, - tags: tags, - page: page, - )) - .recipesList; + return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList; } catch (e) { rethrow; } } - Future getDiets({ - int page = 1, - String? query, - String? types, - int? duration, - String? sorting, - }) async { + Future getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async { try { - return (await feedService.getDiets( - page: page, - query: query, - types: types, - duration: duration, - sorting: sorting, - )); + return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting)); } catch (e) { rethrow; } } - Future getPlans({ - int page = 1, - String? query, - String? types, - int? duration, - int? minPrice, - int? maxPrice, - }) async { + Future getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async { try { - return (await feedService.getPlans( - page: page, - query: query, - types: types, - duration: duration, - minPrice: minPrice, - maxPrice: maxPrice, - )); + return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)); } catch (e) { rethrow; } } - Future getBlogPostComments({ - required String id, - }) async { + Future getBlogPostComments({required String id}) async { try { return (await feedService.getBlogPostComments(id: id)); } catch (e) { @@ -109,25 +61,15 @@ class FeedRepository { } } - Future sendBlogPostComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendBlogPostComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getDietsComments({ - required String id, - }) async { + Future getDietsComments({required String id}) async { try { return (await feedService.getDietsComments(id: id)); } catch (e) { @@ -135,25 +77,15 @@ class FeedRepository { } } - Future sendDietsComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendDietsComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendDietsComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getRecipesComments({ - required String id, - }) async { + Future getRecipesComments({required String id}) async { try { return (await feedService.getRecipesComments(id: id)); } catch (e) { @@ -161,25 +93,15 @@ class FeedRepository { } } - Future sendRecipesComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendRecipesComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getPlansComments({ - required String slug, - }) async { + Future getPlansComments({required String slug}) async { try { return (await feedService.getPlansComments(slug: slug)); } catch (e) { @@ -187,25 +109,15 @@ class FeedRepository { } } - Future sendPlansComment({ - required String slug, - required String commentText, - int? parentCommentId, - }) async { + Future sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async { try { - await feedService.sendPlansComment( - slug: slug, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future likeBlogPost({ - required int id, - }) async { + Future likeBlogPost({required int id}) async { try { await feedService.likeBlogPost(id: id); } catch (e) { @@ -213,9 +125,7 @@ class FeedRepository { } } - Future unlikedBlogPost({ - required int id, - }) async { + Future unlikedBlogPost({required int id}) async { try { await feedService.unlikedBlogPost(id: id); } catch (e) { @@ -262,4 +172,12 @@ class FeedRepository { rethrow; } } + + Future viewContent({required ViewModel viewModel}) async { + try { + await feedService.viewContent(viewModel: viewModel); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index cb5de3d0..265c2d38 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:retrofit/retrofit.dart'; part 'feed_service.g.dart'; @@ -131,4 +132,9 @@ abstract class FeedService { Future viewPlan({ @Path() required String slug, }); + + @POST('/api/views') + Future viewContent({ + @Body() required ViewModel viewModel, + }); } diff --git a/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart b/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart index 6f137211..67d4652f 100644 --- a/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart +++ b/lib/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart @@ -33,21 +33,12 @@ class _UserPurchasesScreen extends State { return Scaffold( backgroundColor: const Color(0xFFF0F2F5), appBar: AppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.vertical( - bottom: Radius.circular(12), - ), - ), - title: Text( - 'Финансы', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))), + title: Text('Финансы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), centerTitle: true, ), body: state.isLoading - ? const Center( - child: Loader(), - ) + ? const Center(child: Loader()) : SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( @@ -67,10 +58,7 @@ class _UserPurchasesScreen extends State { .map( (value) => DropdownMenuItem( value: value, - child: Text( - value.name, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), + child: Text(value.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), ) .toList(), @@ -105,60 +93,41 @@ class _UserPurchasesScreen extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Builder(builder: (context) { - final idLen = state.purchasesModels[index].bankId.length; - return Text( - '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ); - }), + Builder( + builder: (context) { + final idLen = state.purchasesModels[index].bankId.length; + return Text( + '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ); + }, + ), Text( - state.purchasesModels[index].startDate.format('yyyy.MM.dd'), - style: const TextStyle( - color: Color(0xFF6D6F71), - height: 1, - fontWeight: FontWeight.w500, - ), + state.purchasesModels[index].startDate.format('dd.MM.yyyy'), + style: const TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 8), Text( state.purchasesModels[index].title, - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Продавец: ${state.purchasesModels[index].consultantName}', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Text( '${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Container( height: 1, width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFDEDEDF), - ), + decoration: const BoxDecoration(color: Color(0xFFDEDEDF)), ), const SizedBox(height: 8), Row( @@ -166,16 +135,9 @@ class _UserPurchasesScreen extends State { children: [ Text( '${state.purchasesModels[index].paymentPrice} ₽', - style: const TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - StatusWidget( - status: purchaseStatus, + style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), + StatusWidget(status: purchaseStatus), ], ), ], @@ -189,10 +151,7 @@ class _UserPurchasesScreen extends State { else const Padding( padding: EdgeInsets.symmetric(vertical: 40.0), - child: Text( - 'У вас нет таких консультаций', - style: TextStyles.textMMedium, - ), + child: Text('У вас нет таких консультаций', style: TextStyles.textMMedium), ), ], ), @@ -200,7 +159,8 @@ class _UserPurchasesScreen extends State { ), ), ], - )), + ), + ), ); }, ), diff --git a/lib/features/my_plans/presentation/my_plans_screen.dart b/lib/features/my_plans/presentation/my_plans_screen.dart index 3ca9ca3e..f7bdb456 100644 --- a/lib/features/my_plans/presentation/my_plans_screen.dart +++ b/lib/features/my_plans/presentation/my_plans_screen.dart @@ -12,7 +12,6 @@ import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_botto import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart'; -import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -64,9 +63,7 @@ class MyPlansScreenContent extends StatelessWidget { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), backgroundColor: appTheme.gray100, foregroundColor: appTheme.gray100, automaticallyImplyLeading: false, @@ -76,21 +73,12 @@ class MyPlansScreenContent extends StatelessWidget { snap: true, scrolledUnderElevation: 0, bottom: PreferredSize( - preferredSize: Size( - MediaQuery.sizeOf(context).width, - 64, - ), + preferredSize: Size(MediaQuery.sizeOf(context).width, 32), child: Padding( padding: const EdgeInsets.only(bottom: 0), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: MyPlansTab(), - ), - const SizedBox(height: 8), Row( children: [ const SizedBox(width: 4), @@ -104,20 +92,14 @@ class MyPlansScreenContent extends StatelessWidget { _ => activePlansSearchController, }, onChanged: (query) { - EasyDebounce.debounce( - 'searchPlans', - const Duration(milliseconds: 500), - () { - context.read().loadPlans(query: query == '' ? null : query); - }, - ); + EasyDebounce.debounce('searchPlans', const Duration(milliseconds: 500), () { + context.read().loadPlans(query: query == '' ? null : query); + }); }, ), ), const SizedBox(width: 12), - AddItemButton( - onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name), - ), + AddItemButton(onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name)), const SizedBox(width: 4), ], ), @@ -134,10 +116,7 @@ class MyPlansScreenContent extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, MyPlansState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.error != null) { return DataErrorWidget(onReload: () => context.read().loadPlans()); } else { @@ -162,11 +141,7 @@ class MyPlansScreenContent extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: PlansFilterBottomSheet( - myPlansCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(myPlansCubit: context.read())), ), ], ); diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index ce91acdc..7ecdc653 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -14,16 +14,13 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_svg/svg.dart'; +// Removed SVG icon usage after switching to text buttons import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class MyPlanWidget extends StatefulWidget { final PlanModel plan; - const MyPlanWidget({ - super.key, - required this.plan, - }); + const MyPlanWidget({super.key, required this.plan}); @override State createState() => _MyPlanWidgetState(); @@ -34,10 +31,7 @@ class _MyPlanWidgetState extends State { Widget build(BuildContext context) { final s = S.of(context); return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -48,23 +42,14 @@ class _MyPlanWidgetState extends State { onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.plan.image}', - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - ), + child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), ), ), Positioned( bottom: 16, child: SizedBox( width: MediaQuery.sizeOf(context).width, - child: InfoRow( - plan: widget.plan, - itemsBackgroundColor: appTheme.whiteA700, - disableDescriptionOfTypes: true, - type: ProductType.plan, - ), + child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan), ), ), ], @@ -75,19 +60,16 @@ class _MyPlanWidgetState extends State { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - widget.plan.name, - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), + child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), ), if (widget.plan.details.isNotEmpty) ...[ - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Html( - data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '
')}...' : widget.plan.details.replaceAll('\n', '
'), + data: widget.plan.details.length > 200 + ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '
')}...' + : widget.plan.details.replaceAll('\n', '
'), style: { "*": Style( margin: Margins.zero, @@ -116,26 +98,61 @@ class _MyPlanWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ + // Buttons aligned with MyRecipeWidget: Delete (gray) and Share (blue) Row( children: [ Expanded( child: CustomOutlinedButton( - text: switch (state.selectedStateIndex) { - 1 => s.publish, - _ => s.edit, + onPressed: () async { + final result = await context.read().deletePlan(widget.plan.slug); + if (result) { + Future.delayed(const Duration(seconds: 3), () { + if (!context.mounted) return; + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + const SizedBox(height: 20), + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text(widget.plan.name, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + Text(s.deleted, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }); + } }, + text: 'Удалить', height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), side: const WidgetStatePropertyAll(BorderSide.none), ), ), ), - if (state.selectedStateIndex == 0) ...[ - const SizedBox(width: 6), - CustomOutlinedButton( + const SizedBox(width: 8), + Expanded( + child: CustomOutlinedButton( onPressed: () async { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( @@ -146,109 +163,14 @@ class _MyPlanWidgetState extends State { ); }, height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/share2.svg', - width: 20, - height: 20, - colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none), ), - text: '', + text: s.share, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), ), - ], - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: CustomOutlinedButton( - text: switch (state.selectedStateIndex) { - 0 => s.removeFromActive, - _ => s.edit, - }, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - ), - ), - const SizedBox(width: 6), - CustomOutlinedButton( - onPressed: () async { - final result = await context.read().deletePlan(widget.plan.slug); - if (result) { - Future.delayed(const Duration(seconds: 3), () { - if (!context.mounted) return; - showBottomSheetWithBlur( - context: context, - child: Column( - children: [ - const SizedBox( - height: 20, - ), - const BottomSheetHandlerBar(), - const SizedBox( - height: 20, - ), - Text( - widget.plan.name, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - s.deleted, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - const SizedBox( - height: 20, - ), - ], - ), - ); - }); - } - }, - height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/trash_can.svg', - width: 20, - height: 20, - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - text: '', ), ], ), diff --git a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart index c681efd1..2635bef1 100644 --- a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart +++ b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart @@ -1,71 +1,3 @@ -import 'package:eatvio_app/core/theme/custom_text_style.dart'; -import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/core/widgets/animated_option_button.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; -import 'package:eatvio_app/generated/l10n.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +// File intentionally left minimal after removing tabs for My Plans -class MyPlansTab extends StatefulWidget { - const MyPlansTab({super.key, this.additionalOnTap}); - final void Function()? additionalOnTap; - @override - State createState() => _MyPlansTabState(); -} - -class _MyPlansTabState extends State { - final List tabTitles = []; - @override - void didChangeDependencies() { - final s = S.of(context); - if (tabTitles.isEmpty) { - tabTitles.add(s.published); - tabTitles.add(s.drafted); - } - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Align( - alignment: Alignment.centerLeft, - child: Row( - children: [ - Expanded( - child: Container( - height: 48, - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: BorderRadius.circular(100), - ), - padding: const EdgeInsets.all(4), - child: Row( - children: [ - ...List.generate( - 2, - (index) => Expanded( - child: AnimatedOptionButton( - textStyle: TextStyles.textSMedium, - isSelected: state.selectedStateIndex == index, - text: tabTitles[index], - onTap: () { - widget.additionalOnTap?.call(); - context.read().changeSelectedStateIndex(index); - }, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - }, - ); - } -} +// Drafts/published tab UI removed per requirements diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index ed196b00..6b675382 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -307,7 +307,7 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), onPressed: () { - GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); + GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId); }, ), ], 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 d0d7a932..5ad4a948 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -18,11 +18,7 @@ class ProfileCubit extends Cubit { final ConsultantRepository consultantRepository; String _currentPeriod = '1'; - ProfileCubit({ - required this.profileRepository, - required this.chatRepository, - required this.consultantRepository, - }) : super(const ProfileState.loading()); + ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading()); Future getProfileBySlug(String slug) async { emit(const ProfileState.loading()); @@ -42,9 +38,7 @@ class ProfileCubit extends Cubit { } } - Future getMyProfile({ - String period = '1', - }) async { + Future getMyProfile({String period = '1'}) async { emit(const ProfileState.loading()); _currentPeriod = period; try { @@ -77,15 +71,14 @@ class ProfileCubit extends Cubit { profile: (state as ProfileStateLoaded).profile, profileDetails: (state as ProfileStateLoaded).profileDetails, blogPosts: feedBlogPostList, + weightPoints: (state as ProfileStateLoaded).weightPoints, + circumferencePoints: (state as ProfileStateLoaded).circumferencePoints, + consultantInfo: (state as ProfileStateLoaded).consultantInfo, ), ); } - Future addCircumference({ - required int chestCircumference, - required int waistCircumference, - required int hipCircumference, - }) async { + Future addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async { try { await profileRepository.addCircumference( chestCircumference: chestCircumference, @@ -98,9 +91,7 @@ class ProfileCubit extends Cubit { } } - Future addWeight({ - required double weight, - }) async { + Future addWeight({required double weight}) async { try { await profileRepository.addWeight(weight: weight); await getMyProfile(period: _currentPeriod); @@ -109,25 +100,15 @@ class ProfileCubit extends Cubit { } } - Future getChatRoomIdBySlug({ - required Author author, - }) async { + Future getChatRoomIdBySlug({required Author author}) async { try { final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug); if (chatIdOrError == '404') { - AppRouter.navigatorKey.currentContext?.pushNamed( - 'emptyChatScreen', - extra: author, - ); + AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: author); } else if (chatIdOrError == 'error') { showErrorToast('Ошибка создания чата'); } else { - AppRouter.navigatorKey.currentContext?.pushNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError}); return chatIdOrError; } } catch (e, st) { diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 990beed1..98966234 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/app_router.dart'; @@ -93,7 +92,7 @@ class _ProfileScreenState extends State { } isFirstFetch = false; } - + // Проверяем, заблокировал ли пользователь меня if (state.profile.isBlockedByUser) { return Scaffold( @@ -104,20 +103,12 @@ class _ProfileScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.block, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.block, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Пользователь ограничил доступ к своей странице', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]), ), ], ), @@ -125,7 +116,7 @@ class _ProfileScreenState extends State { ), ); } - + return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -211,6 +202,14 @@ class _ProfileScreenState extends State { if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state), + if (state.blogPosts.isNotEmpty) + const Padding( + padding: EdgeInsets.only(left: 8, top: 16, bottom: 8), + child: Text( + 'Блог', + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), + ), + ), _blogPosts(state), ], ), @@ -277,9 +276,9 @@ class _ProfileScreenState extends State { _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.roles.contains('consultant')) _socialLinks(state), + Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant')) _education(state), if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), - Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), _newBlogPostButton(), _blogPosts(state), @@ -512,6 +511,7 @@ class _ProfileScreenState extends State { children: [ ListView.separated( shrinkWrap: true, + padding: const EdgeInsets.all(0), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => _educationBlock( state.consultantInfo!.educationModel.completedCourses![index].values.first.title, @@ -766,7 +766,7 @@ class _ProfileScreenState extends State { ListView _blogPosts(ProfileStateLoaded state) { return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.only(bottom: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart index ae498824..2a64e2e9 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -1,5 +1,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; @@ -11,16 +13,20 @@ class SettingsCubit extends Cubit { final ProfileRepository profileRepository; final SettingsRepository settingsRepository; final ConsultantRepository consultantRepository; + final ConsultationsRepository consultationsRepository; SettingsCubit({ - required this.profileRepository, - required this.settingsRepository, + required this.profileRepository, + required this.settingsRepository, required this.consultantRepository, + required this.consultationsRepository, }) : super(const SettingsState.loading()); Future init() async { final profile = await profileRepository.getMyProfile(); late final ConsultationRequestModel? consultantInfo; + late final bool hasActiveConsultation; + try { if (profile.phoneConfirmationState.contains('confirmed')) { consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); @@ -30,6 +36,13 @@ class SettingsCubit extends Cubit { } catch (e) { consultantInfo = null; } + + try { + hasActiveConsultation = await _checkActiveConsultation(); + } catch (e) { + hasActiveConsultation = false; + } + try { emit( SettingsState.loaded( @@ -37,6 +50,7 @@ class SettingsCubit extends Cubit { profileDetails: await profileRepository.getMyProfileDetails(), profile: profile, consultationRequestModel: consultantInfo, + hasActiveConsultation: hasActiveConsultation, ), ); } catch (e) { @@ -44,13 +58,9 @@ class SettingsCubit extends Cubit { } } - Future updateProfileDetails({ - required ProfileDetailsModel profileDetailsModel, - }) async { + Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async { try { - await settingsRepository.updateProfileDetails( - profileDetailsModel: profileDetailsModel, - ); + await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel); } catch (e) { rethrow; } finally { @@ -58,13 +68,9 @@ class SettingsCubit extends Cubit { } } - Future updateProfileSettings({ - required ProfileSettingsModel profileSettingsModel, - }) async { + Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async { try { - await settingsRepository.updateProfileSettings( - profileSettingsModel: profileSettingsModel, - ); + await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel); } catch (e) { rethrow; } finally { @@ -80,4 +86,17 @@ class SettingsCubit extends Cubit { return false; } } + + Future _checkActiveConsultation() async { + try { + final userPurchases = await consultationsRepository.getUserPurchases(null, null); + final activeStates = ChosenPlanStateEnum.getActiveStates(); + + return userPurchases.any((purchase) => + activeStates.contains(purchase.chosenPlanState) + ); + } catch (e) { + return false; + } + } } diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart index f220fe5e..86471f31 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart @@ -16,6 +16,7 @@ abstract class SettingsState with _$SettingsState { required ProfileDetailsModel profileDetails, required ProfileModel profile, ConsultationRequestModel? consultationRequestModel, + required bool hasActiveConsultation, }) = SettingsStateLoaded; const factory SettingsState.error({ diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 1c304f29..ad42a7b4 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -32,6 +32,24 @@ class _SettingsScreenState extends State { late bool bedTimeSwitchState; late bool moodSwitchState; + void _showConsultationWarning() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Предупреждение'), + content: const Text('Во время консультации этот параметр нельзя отключить'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Понятно'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -58,8 +76,8 @@ class _SettingsScreenState extends State { children: [ _additionalInfo(state), const SizedBox(height: 12), - // _notifications(state), - // const SizedBox(height: 12), + _notifications(state), + const SizedBox(height: 12), _becomeConsultantButton(state), _deleteAccountButton(state, settingsCubit), ], @@ -147,76 +165,35 @@ class _SettingsScreenState extends State { ); } - // ignore: unused_element Column _notifications(SettingsStateLoaded state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), + child: Text('При использовании дневника', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)), ), _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { - setState(() { - updatesNotificationsSwitchState = !updatesNotificationsSwitchState; - }); - settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState)); - }, - title: 'Уведомление о новых обновлениях', - trailing: CupertinoSwitch( - value: updatesNotificationsSwitchState, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - setState(() { - updatesNotificationsSwitchState = newValue; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState), - ); - }, - ), - ), - _divider(context), - _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: () { - setState(() { - fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), - ); - }, - title: 'Напоминание о заполнении дневника', - trailing: CupertinoSwitch( - value: fillDiaryNotificationsSwitchState, - activeTrackColor: appTheme.blueA400, - onChanged: (newValue) { - setState(() { - fillDiaryNotificationsSwitchState = newValue; - }); - settingsCubit.updateProfileSettings( - profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState), - ); - }, - ), - ), - _divider(context), - _tile( - padding: const EdgeInsets.symmetric(horizontal: 16), - onTap: () { + if (state.hasActiveConsultation) { + _showConsultationWarning(); + return; + } setState(() { moodSwitchState = !moodSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState)); }, - title: 'Указывать “настроение” в заполнение дневника', + title: 'Указывать "настроение" в заполнение дневника', trailing: CupertinoSwitch( - value: moodSwitchState, + value: state.hasActiveConsultation ? true : moodSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { + if (state.hasActiveConsultation) { + _showConsultationWarning(); + return; + } setState(() { moodSwitchState = newValue; }); @@ -228,16 +205,24 @@ class _SettingsScreenState extends State { _tile( padding: const EdgeInsets.symmetric(horizontal: 16), onTap: () { + if (state.hasActiveConsultation) { + _showConsultationWarning(); + return; + } setState(() { bedTimeSwitchState = !bedTimeSwitchState; }); settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState)); }, - title: 'Указывать “отход ко сну” в заполнение дневника', + title: 'Указывать "отход ко сну" в заполнение дневника', trailing: CupertinoSwitch( - value: bedTimeSwitchState, + value: state.hasActiveConsultation ? true : bedTimeSwitchState, activeTrackColor: appTheme.blueA400, onChanged: (newValue) { + if (state.hasActiveConsultation) { + _showConsultationWarning(); + return; + } setState(() { bedTimeSwitchState = newValue; }); diff --git a/lib/main.dart b/lib/main.dart index 60293ed8..d716f3fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -67,7 +68,10 @@ class MyApp extends StatelessWidget { child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => InternetConnectionCheckerScreen(child: child!), + builder: (context, child) => BlocProvider( + create: (context) => sl(), + child: InternetConnectionCheckerScreen(child: child!), + ), debugShowCheckedModeBanner: false, localizationsDelegates: const [ S.delegate, diff --git a/pubspec.yaml b/pubspec.yaml index acf68969..6861a603 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.12+72 +version: 1.2.13+73 publish_to: 'none' environment: