diff --git a/android/build.gradle b/android/build.gradle
index d2ffbffa..9db63328 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -2,6 +2,7 @@ allprojects {
repositories {
google()
mavenCentral()
+ maven { url 'https://developer.huawei.com/repo/' }
}
}
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 668075b4..8e9edd22 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 = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -539,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 = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -578,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 = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index b05e5fac..7e82c5b2 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 72
+ 74
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart
index 572b56f6..2ba89290 100644
--- a/lib/core/firebase_api.dart
+++ b/lib/core/firebase_api.dart
@@ -6,13 +6,24 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:huawei_push/huawei_push.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future getToken() async {
await _firebaseMessaging.requestPermission();
- return await _firebaseMessaging.getToken();
+
+ String? token = await _firebaseMessaging.getToken();
+
+ if (token == null && Platform.isAndroid) {
+ final isHuawei = await _isHuaweiDevice();
+ if (isHuawei) {
+ token = await _getHuaweiPushToken();
+ }
+ }
+
+ return token;
}
Future registerPushToken() async {
@@ -32,4 +43,38 @@ class FirebaseApi {
}
}
}
+
+ Future _isHuaweiDevice() async {
+ if (!Platform.isAndroid) return false;
+ try {
+ final android = await DeviceInfoPlugin().androidInfo;
+ final brand = android.brand.toLowerCase();
+ final manufacturer = android.manufacturer.toLowerCase();
+ return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor');
+ } catch (_) {
+ return false;
+ }
+ }
+
+ Future _getHuaweiPushToken() async {
+ try {
+ await Push.turnOnPush();
+
+ String? resolvedToken;
+ final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {});
+
+ Push.getToken('');
+
+ final started = DateTime.now();
+ while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) {
+ await Future.delayed(const Duration(milliseconds: 100));
+ }
+
+ await sub.cancel();
+ return resolvedToken;
+ } catch (e, st) {
+ log('Huawei Push token error: $e', stackTrace: st);
+ return null;
+ }
+ }
}
diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart
index e4a13866..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';
@@ -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/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart
index c0c0af29..c202129e 100644
--- a/lib/core/theme/theme_helper.dart
+++ b/lib/core/theme/theme_helper.dart
@@ -204,6 +204,8 @@ class PrimaryColors {
Color get commentBg => const Color(0XFFFFFDD8);
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
+
+ Color get grey7B => const Color(0xFF7B7B7B);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();
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..ea457d6c 100644
--- a/lib/features/auth/presentation/pages/auth_screen.dart
+++ b/lib/features/auth/presentation/pages/auth_screen.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: unused_element, duplicate_ignore
+
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
@@ -24,6 +26,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 +168,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 +366,88 @@ class _AuthScreenState extends State {
);
}
}
+
+// ignore: unused_element
+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),
+ ),
+ ),
+ );
+ }
+}
+
+// ignore: unused_element
+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/blog_post/presentation/cubit/blog_post_cubit.dart b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart
index 59665002..02196288 100644
--- a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart
+++ b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart
@@ -18,4 +18,26 @@ class BlogPostCubit extends Cubit {
emit(state.copyWith(isLoading: false));
}
}
+
+ Future toggleLike() async {
+ final current = state.blogPostModel;
+ if (current == null) return;
+
+ // optimistic update
+ final wasLiked = current.wasLikedByMe;
+ final updated = current.copyWith(wasLikedByMe: !wasLiked, totalLikes: wasLiked ? current.totalLikes - 1 : current.totalLikes + 1);
+ emit(state.copyWith(blogPostModel: updated));
+
+ try {
+ if (wasLiked) {
+ await feedRepository.unlikedBlogPost(id: current.id);
+ } else {
+ await feedRepository.likeBlogPost(id: current.id);
+ }
+ } catch (e, st) {
+ // rollback on error
+ log(e.toString(), stackTrace: st);
+ emit(state.copyWith(blogPostModel: current));
+ }
+ }
}
diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart
index 25636a57..61b64f9e 100644
--- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart
+++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart
@@ -32,8 +32,6 @@ class _BlogPostScreenState extends State {
final CommentsCubit commentsCubit = sl();
final textEditingController = TextEditingController();
int? replyId;
- bool isLiked = false;
- int totalLikes = 0;
@override
void initState() {
@@ -60,26 +58,30 @@ class _BlogPostScreenState extends State {
builder: (BuildContext context, CommentsState state) {
if (blogPostState.isLoading) {
return Scaffold(
- appBar: AppBar(
- leading: BackButton(
- onPressed: () {
- if (context.canPop()) {
- GoRouter.of(context).pop();
- return;
- }
- GoRouter.of(context).go('/feed-screen');
- },
- ),
+ appBar: AppBar(
+ leading: BackButton(
+ onPressed: () {
+ if (context.canPop()) {
+ GoRouter.of(context).pop();
+ return;
+ }
+ GoRouter.of(context).go('/feed-screen');
+ },
),
- body: const Loader());
+ ),
+ body: const Loader(),
+ );
}
- isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
- totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
+ final bool isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
+ final int totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
- decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
+ ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -114,24 +116,14 @@ class _BlogPostScreenState extends State {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
- style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w400,
- color: Colors.black,
- height: 1.2,
- ),
+ style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
- hintStyle: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w400,
- height: 1.2,
- color: Color(0xFFAEAEB2),
- ),
+ hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -196,7 +188,7 @@ class _BlogPostScreenState extends State {
id: widget.id,
feedBlogPostModel: blogPostState.blogPostModel,
userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
- ),
+ ),
const SizedBox(width: 16),
],
),
@@ -215,9 +207,7 @@ class _BlogPostScreenState extends State {
children: [
CircleAvatar(
radius: 16,
- foregroundImage: NetworkImage(
- baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''),
- ),
+ foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
@@ -242,30 +232,32 @@ class _BlogPostScreenState extends State {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'), style: {"*": Style(color: Colors.black87)}),
+ child: Html(
+ data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'),
+ style: {"*": Style(color: Colors.black87)},
+ ),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
- CachedNetworkImage(
- imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}',
- ),
+ CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
- icon: Icons.favorite_border_rounded,
+ icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
- setState(() {
- isLiked = !isLiked;
- isLiked ? totalLikes++ : totalLikes--;
- });
- isLiked ? await context.read().likePost(widget.id) : await context.read().unlikedPost(widget.id);
- sl().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked);
+ await context.read().toggleLike();
+ // ignore: use_build_context_synchronously
+ final updated = context.read().state.blogPostModel;
+ if (updated != null) {
+ // ignore: use_build_context_synchronously
+ context.read().updateFeedBlogPostList(updated, updated.wasLikedByMe);
+ }
},
),
const SizedBox(width: 8),
@@ -284,8 +276,10 @@ class _BlogPostScreenState extends State {
const Spacer(),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
- Text(blogPostState.blogPostModel?.totalViews.toString() ?? '',
- style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
+ Text(
+ blogPostState.blogPostModel?.totalViews.toString() ?? '',
+ style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
+ ),
],
),
),
@@ -332,7 +326,11 @@ class _BlogPostScreenState extends State {
children: [
Icon(icon, size: 20, color: iconColor ?? appTheme.gray600),
if (text != null) const SizedBox(width: 8),
- if (text != null) Text(text, style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500)),
+ if (text != null)
+ Text(
+ text,
+ style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500),
+ ),
],
),
),
diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
index 760a8783..f9fa4143 100644
--- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
+++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
@@ -1,11 +1,13 @@
import 'package:collection/collection.dart';
-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';
@@ -63,7 +65,7 @@ class _ConsultationCardWidgetState extends State {
children: [
Row(
children: [
- CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)),
+ CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -284,6 +286,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/mood_rating_widget.dart b/lib/features/diary/presentation/widgets/mood_rating_widget.dart
index 41640b78..d943eac2 100644
--- a/lib/features/diary/presentation/widgets/mood_rating_widget.dart
+++ b/lib/features/diary/presentation/widgets/mood_rating_widget.dart
@@ -23,9 +23,12 @@ class _MoodRatingWidgetState extends State {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
final selectedIndex = state.selectedMeal - 1;
- if (isFirstFetch[selectedIndex]) {
- currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].mood.toDouble() + 1;
- isFirstFetch[selectedIndex] = false;
+ final meals = state.diary.meals;
+ if (selectedIndex >= 0 && selectedIndex < meals.length) {
+ if (isFirstFetch[selectedIndex]) {
+ currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1;
+ isFirstFetch[selectedIndex] = false;
+ }
}
return Padding(
padding: const EdgeInsets.only(top: 24),
@@ -35,21 +38,17 @@ class _MoodRatingWidgetState extends State {
Text(
state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи',
textAlign: TextAlign.center,
- style: const TextStyle(
- fontSize: 16,
- ),
+ style: const TextStyle(fontSize: 16),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- SvgPicture.asset(
- 'assets/icons/mood_1.svg',
- height: 32,
- width: 32,
- ),
+ SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32),
RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
- initialRating: state.diary.meals[selectedIndex].mood.toDouble() + 1,
+ initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
+ ? state.diary.meals[selectedIndex].mood.toDouble() + 1
+ : 3,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
@@ -63,20 +62,11 @@ class _MoodRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1
- ? Border.all(
- width: 2,
- color: const Color(0xFFAA3333),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 1
- ? const BorderRadius.horizontal(
- left: Radius.circular(8),
- right: Radius.circular(2),
- )
- : const BorderRadius.horizontal(
- left: Radius.circular(8),
- ),
+ ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
+ : const BorderRadius.horizontal(left: Radius.circular(8)),
),
);
case 1:
@@ -87,11 +77,7 @@ class _MoodRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2
- ? Border.all(
- width: 2,
- color: const Color(0xFF550000),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
),
@@ -104,11 +90,7 @@ class _MoodRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3
- ? Border.all(
- width: 2,
- color: const Color(0xFF8C5D00),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
),
@@ -121,11 +103,7 @@ class _MoodRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4
- ? Border.all(
- width: 2,
- color: const Color(0xFFB5911B),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
),
@@ -138,11 +116,7 @@ class _MoodRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5
- ? Border.all(
- width: 2,
- color: const Color(0xFF276918),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
),
@@ -155,20 +129,11 @@ class _MoodRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6
- ? Border.all(
- width: 2,
- color: const Color(0xFF184D12),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 6
- ? const BorderRadius.horizontal(
- left: Radius.circular(2),
- right: Radius.circular(8),
- )
- : const BorderRadius.horizontal(
- right: Radius.circular(8),
- ),
+ ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
+ : const BorderRadius.horizontal(right: Radius.circular(8)),
),
);
}
@@ -177,20 +142,12 @@ class _MoodRatingWidgetState extends State {
setState(() {
currentSelectedRating[selectedIndex] = value;
});
- EasyDebounce.debounce(
- 'change-satiety',
- const Duration(milliseconds: 250),
- () {
- context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
- },
- );
+ EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
+ context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
+ });
},
),
- SvgPicture.asset(
- 'assets/icons/mood_2.svg',
- height: 32,
- width: 32,
- ),
+ SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32),
],
),
],
diff --git a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart
index d473f691..d36ac75b 100644
--- a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart
+++ b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart
@@ -23,9 +23,12 @@ class _SatietyRatingWidgetState extends State {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
final selectedIndex = state.selectedMeal - 1;
- if (isFirstFetch[selectedIndex]) {
- currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].satiety.toDouble() + 1;
- isFirstFetch[selectedIndex] = false;
+ final meals = state.diary.meals;
+ if (selectedIndex >= 0 && selectedIndex < meals.length) {
+ if (isFirstFetch[selectedIndex]) {
+ currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1;
+ isFirstFetch[selectedIndex] = false;
+ }
}
return Padding(
padding: const EdgeInsets.only(top: 24),
@@ -35,21 +38,17 @@ class _SatietyRatingWidgetState extends State {
Text(
state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи',
textAlign: TextAlign.center,
- style: const TextStyle(
- fontSize: 16,
- ),
+ style: const TextStyle(fontSize: 16),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- SvgPicture.asset(
- 'assets/icons/satiety_emoji_left.svg',
- height: 32,
- width: 32,
- ),
+ SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32),
RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
- initialRating: state.diary.meals[selectedIndex].satiety.toDouble() + 1,
+ initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
+ ? state.diary.meals[selectedIndex].satiety.toDouble() + 1
+ : 3,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
@@ -63,20 +62,11 @@ class _SatietyRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1
- ? Border.all(
- width: 2,
- color: const Color(0xFFAA3333),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 1
- ? const BorderRadius.horizontal(
- left: Radius.circular(8),
- right: Radius.circular(2),
- )
- : const BorderRadius.horizontal(
- left: Radius.circular(8),
- ),
+ ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
+ : const BorderRadius.horizontal(left: Radius.circular(8)),
),
);
case 1:
@@ -87,11 +77,7 @@ class _SatietyRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2
- ? Border.all(
- width: 2,
- color: const Color(0xFF550000),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
),
@@ -104,11 +90,7 @@ class _SatietyRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3
- ? Border.all(
- width: 2,
- color: const Color(0xFF8C5D00),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
),
@@ -121,11 +103,7 @@ class _SatietyRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4
- ? Border.all(
- width: 2,
- color: const Color(0xFFB5911B),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
),
@@ -138,11 +116,7 @@ class _SatietyRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5
- ? Border.all(
- width: 2,
- color: const Color(0xFF276918),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
),
@@ -155,20 +129,11 @@ class _SatietyRatingWidgetState extends State {
decoration: BoxDecoration(
color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6
- ? Border.all(
- width: 2,
- color: const Color(0xFF184D12),
- strokeAlign: BorderSide.strokeAlignOutside,
- )
+ ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 6
- ? const BorderRadius.horizontal(
- left: Radius.circular(2),
- right: Radius.circular(8),
- )
- : const BorderRadius.horizontal(
- right: Radius.circular(8),
- ),
+ ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
+ : const BorderRadius.horizontal(right: Radius.circular(8)),
),
);
}
@@ -178,20 +143,12 @@ class _SatietyRatingWidgetState extends State {
setState(() {
currentSelectedRating[selectedIndex] = value;
});
- EasyDebounce.debounce(
- 'change-satiety',
- const Duration(milliseconds: 250),
- () {
- context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
- },
- );
+ EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
+ context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
+ });
},
),
- SvgPicture.asset(
- 'assets/icons/satiety_emoji_right.svg',
- height: 32,
- width: 32,
- ),
+ SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32),
],
),
],
diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart
index d385ed75..ddc9c74a 100644
--- a/lib/features/diary/repositories/diary_repository.dart
+++ b/lib/features/diary/repositories/diary_repository.dart
@@ -75,7 +75,53 @@ class DiaryRepository {
}
Future getDiaryDay(int diaryId) async {
- return (await diaryService.getDiaryDay(diary: diaryId)).diary;
+ // Align with getDiaryByDate: enrich portions with glossary nutritional info
+ final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
+ final DiaryDayModel diaryDay = diaryWrapper.diary;
+ Set ids = {};
+ for (var meal in diaryDay.meals) {
+ for (var portion in meal.portions) {
+ if (portion.productNutritionalValueId != null) {
+ ids.add(portion.productNutritionalValueId!);
+ }
+ }
+ }
+ if (ids.isNotEmpty) {
+ final glossary = await getGlossariesIndex(ids: ids.toList());
+ final List updatedMeals = List.from(diaryDay.meals);
+ for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
+ final meal = updatedMeals[mealIndex];
+ final updatedPortions = meal.portions.map((portion) {
+ if (portion.productNutritionalValueId != null) {
+ try {
+ final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
+ return portion.copyWith(
+ productCaloriesInfo: ProductCaloriesInfoModel(
+ id: glossaryItem.id,
+ productName: glossaryItem.name,
+ calories: glossaryItem.energyOn100Gr,
+ proteins: glossaryItem.proteinsOn100Gr,
+ fats: glossaryItem.fatsOn100Gr,
+ carbohydrates: glossaryItem.carbohydratesOn100Gr,
+ energyOnPortion: glossaryItem.energyOnPortion,
+ proteinsOnPortion: glossaryItem.proteinsOnPortion,
+ fatsOnPortion: glossaryItem.fatsOnPortion,
+ carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
+ ),
+ );
+ } catch (e) {
+ return portion;
+ }
+ }
+ return portion;
+ }).toList();
+
+ updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
+ }
+ final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
+ return updatedDiaryDay;
+ }
+ return diaryDay;
}
Future getNutritionData(int index) async {
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..d1bf399f 100644
--- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart
+++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart
@@ -15,13 +15,12 @@ class FeedCubit extends Cubit {
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
- if (feedBlogPostModel == null || isClosed) return;
- List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
- final int index = feedBlogPostList.indexOf(feedBlogPostModel);
- feedBlogPostList[index] = feedBlogPostModel.copyWith(
- wasLikedByMe: wasLikedByMe,
- totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1,
- );
+ if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return;
+ final List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
+ final int index = feedBlogPostList.indexWhere((e) => e.id == feedBlogPostModel.id);
+ if (index == -1) return;
+ final current = feedBlogPostList[index];
+ feedBlogPostList[index] = current.copyWith(wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe ? current.totalLikes + 1 : current.totalLikes - 1);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
}
diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart
index ce29b884..796b4a3b 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';
@@ -35,7 +36,6 @@ class FeedScreen extends StatelessWidget {
providers: [
BlocProvider(create: (context) => sl()..loadRecipesList()),
BlocProvider(create: (context) => sl()..getProfile()),
- BlocProvider(create: (context) => sl()..loadFeedBlogPostList()),
BlocProvider(create: (context) => sl()..loadDiets()),
if (!hidePlans) BlocProvider(create: (context) => sl()..loadPlans()),
BlocProvider(create: (context) => sl()..getConsultants()),
@@ -66,12 +66,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();
}
@@ -228,10 +225,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 +237,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_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart
index 23eaeded..e001a038 100644
--- a/lib/features/feed/presentation/widgets/blog_post_widget.dart
+++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart
@@ -21,10 +21,7 @@ import 'package:share_plus/share_plus.dart';
class BlogPostWidget extends StatefulWidget {
final int index;
- const BlogPostWidget({
- super.key,
- required this.index,
- });
+ const BlogPostWidget({super.key, required this.index});
@override
State createState() => _BlogPostWidgetState();
@@ -44,10 +41,7 @@ class _BlogPostWidgetState extends State {
totalLikes = state.feedBlogPostList[widget.index].totalLikes;
return InkWell(
borderRadius: BorderRadius.circular(12),
- onTap: () => GoRouter.of(context).pushNamed(
- 'blogPostScreen',
- extra: state.feedBlogPostList[widget.index].id,
- ),
+ onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -95,21 +89,12 @@ class _BlogPostWidgetState extends State {
Text(
state.feedBlogPostList[widget.index].author.authorName,
overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- height: 1,
- color: Colors.black87,
- fontWeight: FontWeight.w500,
- fontSize: 16,
- ),
+ style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
- style: const TextStyle(
- height: 1,
- color: Colors.black45,
- fontSize: 14,
- ),
+ style: const TextStyle(height: 1, color: Colors.black45, fontSize: 14),
),
],
),
@@ -119,9 +104,7 @@ class _BlogPostWidgetState extends State {
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
- child: SubscribeButtonWidget(
- slug: state.feedBlogPostList[widget.index].author.authorSlug,
- ),
+ child: SubscribeButtonWidget(slug: state.feedBlogPostList[widget.index].author.authorSlug),
),
// Кнопка меню
CustomPopup(
@@ -141,11 +124,7 @@ class _BlogPostWidgetState extends State {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.feedBlogPostList[widget.index].title,
- style: const TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- color: Colors.black87,
- ),
+ style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87),
),
),
],
@@ -156,15 +135,12 @@ class _BlogPostWidgetState extends State {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
- data: (state.feedBlogPostList[widget.index].description.length > 200
- ? state.feedBlogPostList[widget.index].description.substring(0, 200)
- : state.feedBlogPostList[widget.index].description).replaceAll('\n', '
'),
- style: {
- "*": Style(
- fontSize: FontSize(16),
- color: Colors.black87,
- ),
- },
+ data:
+ (state.feedBlogPostList[widget.index].description.length > 200
+ ? state.feedBlogPostList[widget.index].description.substring(0, 200)
+ : state.feedBlogPostList[widget.index].description)
+ .replaceAll('\n', '
'),
+ style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
),
),
if (state.feedBlogPostList[widget.index].description.length > 200)
@@ -172,12 +148,7 @@ class _BlogPostWidgetState extends State {
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'Показать ещё',
- style: TextStyle(
- height: 1,
- color: Color(0xFF8B8B8B),
- fontSize: 14,
- fontWeight: FontWeight.w600,
- ),
+ style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty)
@@ -186,10 +157,7 @@ class _BlogPostWidgetState extends State {
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
- PhotoViewScreen(
- imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'],
- index: 0,
- ),
+ PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0),
);
},
child: Hero(
@@ -218,11 +186,14 @@ class _BlogPostWidgetState extends State {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
- isLiked
- ? await context.read().likePost(state.feedBlogPostList[widget.index].id)
- : await context.read().unlikedPost(state.feedBlogPostList[widget.index].id);
- if (!context.mounted) return;
- await context.read().loadFeedBlogPostList();
+ // Update feed list immediately by ID (no reload)
+ context.read().updateFeedBlogPostList(state.feedBlogPostList[widget.index], isLiked);
+ // Fire network call without introducing UI delay
+ if (isLiked) {
+ await context.read().likePost(state.feedBlogPostList[widget.index].id);
+ } else {
+ await context.read().unlikedPost(state.feedBlogPostList[widget.index].id);
+ }
},
),
const SizedBox(width: 8),
@@ -231,16 +202,10 @@ class _BlogPostWidgetState extends State {
'assets/icons/comment.svg',
height: 20,
width: 20,
- colorFilter: ColorFilter.mode(
- appTheme.gray600,
- BlendMode.srcIn,
- ),
+ colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
text: state.feedBlogPostList[widget.index].totalComments.toString(),
- onTap: () => GoRouter.of(context).pushNamed(
- 'blogPostScreen',
- extra: state.feedBlogPostList[widget.index].id,
- ),
+ onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -254,28 +219,18 @@ class _BlogPostWidgetState extends State {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
- uri: Uri.parse(
- '$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',
- ),
+ uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
),
const Spacer(),
- Icon(
- Icons.visibility_rounded,
- size: 16,
- color: appTheme.gray500,
- ),
+ Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
state.feedBlogPostList[widget.index].totalViews.toString(),
- style: TextStyle(
- fontSize: 12,
- color: appTheme.gray500,
- fontWeight: FontWeight.w400,
- ),
+ style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),
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/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/models/profile_model.dart b/lib/features/profile/models/profile_model.dart
index 03fd57f3..17eaa671 100644
--- a/lib/features/profile/models/profile_model.dart
+++ b/lib/features/profile/models/profile_model.dart
@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
+ @JsonKey(name: "is_blocked_by_author", fromJson: parseBool) required bool isBlockedByAuthor,
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug,
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 1ea45b7c..758a6c54 100644
--- a/lib/features/profile/presentation/pages/profile_screen.dart
+++ b/lib/features/profile/presentation/pages/profile_screen.dart
@@ -1,11 +1,11 @@
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';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
+import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -200,11 +200,34 @@ 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') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(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)
+ if (state.profile.isBlockedByAuthor)
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.block, size: 48, color: appTheme.grey7B),
+ const SizedBox(height: 16),
+ Center(
+ child: Text(
+ 'Пользователь ограничил доступ\nк своей странице',
+ textAlign: TextAlign.center,
+ style: TextStyles.textMSemibold.copyWith(color: appTheme.grey7B),
+ ),
+ ),
+ ],
+ ),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
+ _socialLinks(state),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
+ _education(state),
+ if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
+ _diplomasAndCertificates(state),
+ if (state.profile.roles.contains('consultant') &&
+ state.consultantInfo != null &&
+ !hidePlans &&
+ !state.profile.isBlockedByMe &&
+ !state.profile.isBlockedByAuthor)
+ _activePlans(state),
+ if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe)
const Padding(
padding: EdgeInsets.only(left: 8, top: 16, bottom: 8),
child: Text(
@@ -212,7 +235,7 @@ class _ProfileScreenState extends State {
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600),
),
),
- _blogPosts(state),
+ if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) _blogPosts(state),
],
),
),
diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart
index 4907e3dd..46e986f4 100644
--- a/lib/features/user_details/presentation/user_details_screen.dart
+++ b/lib/features/user_details/presentation/user_details_screen.dart
@@ -231,7 +231,7 @@ class _UserDetailsContentState extends State {
children: [
Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
- CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())),
+ CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo ? 'да' : 'нет')),
],
),
),
@@ -242,7 +242,7 @@ class _UserDetailsContentState extends State {
children: [
Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
- CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())),
+ CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper ? 'да' : 'нет')),
],
),
),
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..3f1e036e 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.14+74
publish_to: 'none'
environment:
@@ -74,6 +74,7 @@ dependencies:
flutter_dotenv: ^5.2.1
uuid: ^4.5.1
appmetrica_plugin: ^3.3.0
+ huawei_push: ^6.11.0+300
dev_dependencies:
build_runner: ^2.7.0
diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart
index 8c666344..5c773bc3 100644
--- a/test/features/profile/services/profile_service_test.dart
+++ b/test/features/profile/services/profile_service_test.dart
@@ -30,6 +30,7 @@ void main() {
badges: [],
subscriptions: [],
subscribers: [],
+ isBlockedByAuthor: false,
),
),
);
@@ -72,23 +73,17 @@ void main() {
});
test('getBlogPostByAuthorSlug', () async {
- when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer(
- (_) async => [],
- );
+ when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA>());
});
test('getWeight', () async {
- when(mockProfileService.getWeight(period: '')).thenAnswer(
- (_) async => [],
- );
+ when(mockProfileService.getWeight(period: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getWeight(period: ''), isA>());
});
test('getCircumferences', () async {
- when(mockProfileService.getCircumferences(period: '')).thenAnswer(
- (_) async => [],
- );
+ when(mockProfileService.getCircumferences(period: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getCircumferences(period: ''), isA>());
});
});