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: