forked from eatvio/eatvio_app
changed logic of view requests sending
This commit is contained in:
@@ -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: '');
|
||||
|
||||
|
||||
@@ -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<void> init() async {
|
||||
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
|
||||
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
|
||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
|
||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
@@ -212,6 +216,7 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
|
||||
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
@@ -247,6 +252,7 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
|
||||
128
lib/core/views/cubit/views_cubit.dart
Normal file
128
lib/core/views/cubit/views_cubit.dart
Normal file
@@ -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<ViewsCubitState> {
|
||||
final ViewsRepository viewsRepository;
|
||||
ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) {
|
||||
_listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews);
|
||||
}
|
||||
|
||||
late final AppLifecycleListener _listener;
|
||||
|
||||
final Set<int> postsIds = <int>{};
|
||||
final Set<int> plansIds = <int>{};
|
||||
final Set<int> dietsIds = <int>{};
|
||||
final Set<int> recipesIds = <int>{};
|
||||
|
||||
final Set<int> _sentPosts = <int>{};
|
||||
final Set<int> _sentPlans = <int>{};
|
||||
final Set<int> _sentDiets = <int>{};
|
||||
final Set<int> _sentRecipes = <int>{};
|
||||
|
||||
final Map<ViewableContentType, Map<int, Timer>> _visibilityTimers = {
|
||||
ViewableContentType.posts: <int, Timer>{},
|
||||
ViewableContentType.plans: <int, Timer>{},
|
||||
ViewableContentType.diets: <int, Timer>{},
|
||||
ViewableContentType.recipes: <int, Timer>{},
|
||||
};
|
||||
|
||||
Timer? _debounceTimer;
|
||||
|
||||
Set<int> _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<int> _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<void> _viewContent({required ViewableContentType type, required List<int> 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<int> ids, Set<int> 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<void> 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();
|
||||
}
|
||||
}
|
||||
3
lib/core/views/cubit/views_state.dart
Normal file
3
lib/core/views/cubit/views_state.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class ViewsCubitState {
|
||||
ViewsCubitState();
|
||||
}
|
||||
16
lib/core/views/repository/views_repository.dart
Normal file
16
lib/core/views/repository/views_repository.dart
Normal file
@@ -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<void> viewContent({required ViewModel viewModel}) async {
|
||||
try {
|
||||
await viewsService.viewContent(viewModel: viewModel);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/core/views/service/views_service.dart
Normal file
15
lib/core/views/service/views_service.dart
Normal file
@@ -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<void> viewContent({
|
||||
@Body() required ViewModel viewModel,
|
||||
});
|
||||
}
|
||||
@@ -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<FeedState> {
|
||||
Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id);
|
||||
Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug);
|
||||
|
||||
Future<void> viewContent({required ViewableContentType type, List<int>? ids, List<String>? slugs}) async {
|
||||
feedRepository.viewContent(
|
||||
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids, slugs: slugs),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FeedScreenContent> {
|
||||
final consultantsController = TextEditingController();
|
||||
|
||||
late final List<String> 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<FeedCubit>();
|
||||
@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<FeedScreenContent> {
|
||||
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<FeedScreenContent> {
|
||||
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<int> postsIds = <int>{};
|
||||
final Set<int> plansIds = <int>{};
|
||||
final Set<int> dietsIds = <int>{};
|
||||
final Set<int> recipesIds = <int>{};
|
||||
|
||||
final Map<ViewableContentType, Map<int, Timer>> _visibilityTimers = {
|
||||
ViewableContentType.posts: <int, Timer>{},
|
||||
ViewableContentType.plans: <int, Timer>{},
|
||||
ViewableContentType.diets: <int, Timer>{},
|
||||
ViewableContentType.recipes: <int, Timer>{},
|
||||
};
|
||||
|
||||
Set<int> _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<FeedScreenContent> {
|
||||
];
|
||||
},
|
||||
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<ViewsCubit>().handleVisibility),
|
||||
1 => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
2 => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
3 when !hidePlans => Plans(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
3 when hidePlans => const Consultants(),
|
||||
4 when !hidePlans => const Consultants(),
|
||||
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.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<ViewsCubit>(),
|
||||
child: InternetConnectionCheckerScreen(child: child!),
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
|
||||
Reference in New Issue
Block a user