From 087436da1ccbb89d23fa4f16dea3ee789bedc2b1 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 17 Sep 2025 15:13:16 +0300 Subject: [PATCH] changed logic of view requests sending --- lib/core/const.dart | 2 +- lib/core/locator_service.dart | 6 + lib/core/views/cubit/views_cubit.dart | 128 ++++++++++++++++++ lib/core/views/cubit/views_state.dart | 3 + .../views/repository/views_repository.dart | 16 +++ lib/core/views/service/views_service.dart | 15 ++ .../bloc/feed_cubit/feed_cubit.dart | 7 - .../feed/presentation/pages/feed_screen.dart | 83 +----------- .../feed/presentation/widgets/diets.dart | 2 +- .../feed/presentation/widgets/plans.dart | 2 +- .../feed/presentation/widgets/recipes.dart | 2 +- .../presentation/pages/profile_screen.dart | 1 - lib/main.dart | 6 +- 13 files changed, 183 insertions(+), 90 deletions(-) create mode 100644 lib/core/views/cubit/views_cubit.dart create mode 100644 lib/core/views/cubit/views_state.dart create mode 100644 lib/core/views/repository/views_repository.dart create mode 100644 lib/core/views/service/views_service.dart diff --git a/lib/core/const.dart b/lib/core/const.dart index a5f06149..98f7bc4a 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/core/env_config.dart'; -String baseUrl = EnvConfig.devBaseUrl; +String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); 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/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/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 39ccdc6a..947b9a5d 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -1,9 +1,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; -import 'package:eatvio_app/features/feed/models/view_model.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/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -84,9 +82,4 @@ class FeedCubit extends Cubit { Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); - Future viewContent({required ViewableContentType type, List? ids, List? slugs}) async { - feedRepository.viewContent( - viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids, slugs: slugs), - ); - } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 2bfaf897..844788fe 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - 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'; @@ -62,19 +61,15 @@ class _FeedScreenContentState extends State { final consultantsController = TextEditingController(); late final List chipsLabelList; - late final AppLifecycleListener _listener; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); - final _feedCubit = sl(); @override void initState() { - // Initialize chipsLabelList based on hidePlans setting chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; - _listener = AppLifecycleListener(onDetach: _sendViews); consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } @@ -83,20 +78,7 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } - void _sendViews() { - if (postsIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.posts, ids: postsIds.toList()); - } - if (plansIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.plans, ids: plansIds.toList()); - } - if (dietsIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.diets, ids: dietsIds.toList()); - } - if (recipesIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.recipes, ids: recipesIds.toList()); - } - } + @override void dispose() { @@ -105,63 +87,10 @@ class _FeedScreenContentState extends State { dietsScrollController.dispose(); plansScrollController.dispose(); consultantsScrollController.dispose(); - for (final map in _visibilityTimers.values) { - for (final t in map.values) { - t.cancel(); - } - map.clear(); - } - _listener.dispose(); - _sendViews(); super.dispose(); } - final Set postsIds = {}; - final Set plansIds = {}; - final Set dietsIds = {}; - final Set recipesIds = {}; - final Map> _visibilityTimers = { - ViewableContentType.posts: {}, - ViewableContentType.plans: {}, - ViewableContentType.diets: {}, - ViewableContentType.recipes: {}, - }; - - 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; - } - } - - void _handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { - final timers = _visibilityTimers[type]!; - final ids = _idsForType(type); - - if (ids.contains(id)) { - timers[id]?.cancel(); - timers.remove(id); - return; - } - - if (visibleFraction >= 0.5) { - if (timers.containsKey(id)) return; - timers[id] = Timer(const Duration(seconds: 1), () { - ids.add(id); - timers.remove(id); - }); - } else { - timers[id]?.cancel(); - timers.remove(id); - } - } @override Widget build(BuildContext context) { @@ -301,10 +230,10 @@ class _FeedScreenContentState extends State { ]; }, body: switch (_selectedChipIndex) { - 0 => BlogPosts(handleVisibility: _handleVisibility), - 1 => Recipes(handleVisibility: _handleVisibility), - 2 => Diets(handleVisibility: _handleVisibility), - 3 when !hidePlans => Plans(handleVisibility: _handleVisibility), + 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])), diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index c5e5592a..080b1a14 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -42,7 +42,7 @@ class Diets extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.dietsList[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets); }, child: DietsWidget(diet: state.dietsList[index], index: index), ); diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 4a620945..42e107e5 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -42,7 +42,7 @@ class Plans extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.plans[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans); }, child: PlanWidget(plan: state.plans[index], index: index), ); diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index d286949e..1271a493 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -46,7 +46,7 @@ class Recipes extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.recipesList[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes); }, child: RecipesWidget( recipe: state.recipesList[index], diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index cc8f7245..c11cb0b0 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; 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,