feat(pushNotifications): fixed push notifications navigation

This commit is contained in:
misha
2026-03-26 18:06:20 +03:00
parent 7106bde4ae
commit 335e36dd18
8 changed files with 251 additions and 235 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/routing/auth_routes.dart';
import 'package:eatvio_app/core/routing/content_routes.dart';
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
@@ -14,8 +15,8 @@ import 'package:go_router/go_router.dart';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com'];
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com'];
static const List<String> metaSegments = ['ru', 'en', 'api', 'v2'];
static GoRouter appRouter = GoRouter(
initialLocation: RouteConstants.updateScreen,
@@ -30,11 +31,111 @@ class AppRouter {
],
);
static String? _resolveRecipeDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
}
static String? _resolveBlogPostDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
}
static String? _resolvePlanDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
final planId = int.tryParse(contentIdentifier);
if (planId != null) {
developer.log('Deep link: diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
}
static String? _resolveDietDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
}
final initialTabIndex = hidePlans ? 3 : 4;
return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex';
}
static String? _resolveChatDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
} else if (effectiveSegments.length == 3 && effectiveSegments[1] == 'show') {
final contentIdentifier = effectiveSegments[2];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
}
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
static String? _resolveProfileDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/profile-screen?slug=$contentIdentifier';
}
return RouteConstants.myProfileScreen;
}
static String? _resolveBadgeDeepLink(List<String> effectiveSegments) {
return RouteConstants.myProfileScreen;
}
static String? _resolveDiaryDeepLink(List<String> effectiveSegments) {
developer.log('Deep link: diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
static String? _resolveConsultantDiaryCommentDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
static String? _resolveConsultationsDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 3 || effectiveSegments[1] != 'status') return RouteConstants.consultationsScreen;
final consultationId = effectiveSegments[2];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '${RouteConstants.consultationsScreen}?id=$consultationId';
}
static String? _resolveDeepLinkUri(Uri uri) {
final host = uri.host.toLowerCase();
final segments = uri.pathSegments;
// Проверяем, что хост заканчивается на один из разрешенных доменов
final isDeepLink = deepLinkHosts.any((allowedHost) => host == allowedHost || host.endsWith('.$allowedHost'));
if (!isDeepLink) {
@@ -49,26 +150,7 @@ class AppRouter {
return RouteConstants.feedScreen;
}
// Обработка api ссылок (например, из пушей чата)
if (segments.length >= 4 &&
segments[0] == 'api' &&
segments[1] == 'v2' &&
segments[2] == 'chat' &&
segments[3] == 'show') {
developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
// Определяем, есть ли в начале сегмент языка (ru/en). Если есть — пропускаем его.
int startIndex = 0;
if (segments[0] == 'ru' || segments[0] == 'en') {
startIndex = 1;
} else {
developer.log('Deep link without language segment: ${segments[0]}', name: 'AppRouter');
}
final effectiveSegments = segments.sublist(startIndex);
final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList();
if (effectiveSegments.isEmpty) {
developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
@@ -77,76 +159,47 @@ class AppRouter {
final type = effectiveSegments[0];
if (effectiveSegments.length == 3 && type == 'consultations' && effectiveSegments[1] == 'status') {
final consultationId = effectiveSegments[2];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '${RouteConstants.consultationsScreen}?id=$consultationId';
}
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
switch (type) {
case 'recipe':
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet':
{
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
}
case 'chat':
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
case 'profile':
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/profile-screen?slug=$contentIdentifier';
case 'diary':
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
case 'consultant_diary_comment':
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
}
if (effectiveSegments.length == 1) {
if (type == 'chat') {
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
if (type == 'diary') {
developer.log('Deep link: diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
if (type == 'badge') {
developer.log('Deep link: badge → my-profile tab', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.myProfileScreen;
}
switch (type) {
case 'recipe':
final target = _resolveRecipeDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'blog_post':
final target = _resolveBlogPostDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'plan':
final target = _resolvePlanDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'diet':
final target = _resolveDietDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'chat':
final target = _resolveChatDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'profile':
final target = _resolveProfileDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'badge':
final target = _resolveBadgeDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'diary':
final target = _resolveDiaryDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'consultant_diary_comment':
final target = _resolveConsultantDiaryCommentDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'consultations':
final target = _resolveConsultationsDeepLink(effectiveSegments);
if (target != null) return target;
break;
}
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
@@ -190,7 +243,6 @@ class AppRouter {
}
final target = _resolveDeepLinkUri(uri);
if (target != null) {
appRouter.go(target);
} else {

View File

@@ -1,23 +1,18 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях.
class PushNavigationHandler {
static bool _initialized = false;
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
/// Инициализация обработчиков Firebase Messaging.
/// Должна вызываться один раз после старта приложения.
static Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
// Сообщение, по которому запустили приложение из "убитого" состояния.
try {
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
@@ -26,18 +21,11 @@ class PushNavigationHandler {
});
}
} catch (e) {
developer.log(
'PushNavigationHandler: error while getting initialMessage: $e',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: error while getting initialMessage: $e', name: 'PushNavigationHandler');
}
// Тап по уведомлению, когда приложение было в фоне.
FirebaseMessaging.onMessageOpenedApp.listen(
(message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'),
);
FirebaseMessaging.onMessageOpenedApp.listen((message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'));
// Подписка на события от native-кода (HMS через MethodChannel).
_hmsChannel.setMethodCallHandler((call) async {
if (call.method == 'openActionUrl') {
final url = call.arguments as String? ?? '';
@@ -46,118 +34,31 @@ class PushNavigationHandler {
});
}
/// Входная точка для навигации из native-кода (например, HMS через MethodChannel).
static void handleActionUrlFromNative(String url) {
if (url.isEmpty) {
developer.log(
'PushNavigationHandler.handleActionUrlFromNative: empty url',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler.handleActionUrlFromNative: empty url', name: 'PushNavigationHandler');
return;
}
developer.log(
'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(url);
}
/// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё).
static void handleMessage(RemoteMessage message) {
_handleMessageInternal(message, source: 'manualHandle');
}
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
developer.log(
'PushNavigationHandler: received message. data=${message.data}, notification=${message.notification?.title}, source=$source',
name: 'PushNavigationHandler',
);
final data = message.data;
// Пытаемся взять URL из action_url или action (бэкенд шлёт и то и другое)
String? rawUrl = data['action_url'] as String?;
if (rawUrl == null || rawUrl.isEmpty) {
rawUrl = data['action'] as String?;
}
if (rawUrl != null && rawUrl.isNotEmpty) {
developer.log(
'PushNavigationHandler: manual navigation by url=$rawUrl, source=$source',
name: 'PushNavigationHandler',
);
// Если это URL, но без схемы, добавляем https
String finalUrl = rawUrl;
if (!finalUrl.startsWith('http') && finalUrl.contains('/')) {
finalUrl = 'https://${AppRouter.deepLinkHosts.first}/$finalUrl'.replaceAll('//', '/').replaceFirst('https:/', 'https://');
}
AppRouter.navigateByDeepLinkUrl(finalUrl);
final actionUrl = data['action_url'] as String?;
if (actionUrl == null || actionUrl.isEmpty) {
developer.log('PushNavigationHandler: no action_url in message data, source=$source, data=$data', name: 'PushNavigationHandler');
return;
}
// Fallback: навигация по типу уведомления, если URL отсутствует.
final target = _resolveTargetByType(data);
if (target != null) {
developer.log(
'PushNavigationHandler: fallback navigation by type, target=$target, source=$source, data=$data',
name: 'PushNavigationHandler',
);
AppRouter.appRouter.go(target);
return;
}
developer.log('PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', name: 'PushNavigationHandler');
developer.log(
'PushNavigationHandler: no action_url/action and no known type, source=$source, data=$data',
name: 'PushNavigationHandler',
);
}
/// Определяет маршрут по полям `type` / `action` из data, если URL отсутствует.
static String? _resolveTargetByType(Map<String, dynamic> data) {
final type = data['type'] as String? ?? '';
final action = data['action'] as String? ?? '';
final databaseType = data['database_type'] as String? ?? '';
// Чат
if (type == 'chat_message' || databaseType == 'chat-message') {
return RouteConstants.chatSelectScreen;
}
// Дневник
if (type == 'diary_reminder' || action == 'open_diary' || databaseType == 'diary-reminder') {
return RouteConstants.diaryScreen;
}
// Комментарий консультанта к дневнику
if (type == 'consultant_diary_comment' || databaseType == 'consultant-left-diary-comment') {
return RouteConstants.diaryScreen;
}
// Консультации (plan-related)
if (type.startsWith('plan_') || type.startsWith('chosen_plan_') || databaseType.contains('plan')) {
return RouteConstants.consultationsScreen;
}
// Диета
if (type == 'diet_started' || type == 'diet_cancelled' || databaseType.startsWith('diet-')) {
return RouteConstants.feedScreen;
}
// Бейдж
if (type == 'badge_awarded' || databaseType == 'badge-awarded') {
return RouteConstants.myProfileScreen;
}
// Мотивация
if (type == 'motivation') {
return RouteConstants.feedScreen;
}
return null;
AppRouter.navigateByDeepLinkUrl(actionUrl);
}
}

View File

@@ -33,10 +33,7 @@ class MainNavigationRoutes {
});
}
return _MainShellWithNavScope(
currentLocation: state.matchedLocation,
child: child,
);
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
},
routes: [
GoRoute(
@@ -49,7 +46,10 @@ class MainNavigationRoutes {
GoRoute(
path: RouteConstants.feedScreen,
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
pageBuilder: (context, state) {
final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '0');
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex ?? 0));
},
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
@@ -105,10 +105,7 @@ class MainNavigationRoutes {
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
class _MainShellWithNavScope extends StatefulWidget {
const _MainShellWithNavScope({
required this.child,
required this.currentLocation,
});
const _MainShellWithNavScope({required this.child, required this.currentLocation});
final Widget child;
final String currentLocation;
@@ -124,10 +121,7 @@ class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
Widget build(BuildContext context) {
return BottomNavScope(
controller: _navController,
child: BottomNavigationBarWidget(
currentLocation: widget.currentLocation,
child: widget.child,
),
child: BottomNavigationBarWidget(currentLocation: widget.currentLocation, child: widget.child),
);
}
}

View File

@@ -156,10 +156,7 @@ class OtherRoutes {
builder: (context, state) {
if (state.extra is Map<String, dynamic>) {
final data = state.extra as Map<String, dynamic>;
return RecipeCreateScreen(
recipe: data['recipe'] as Recipe?,
autoValidate: data['autoValidate'] as bool? ?? false,
);
return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false);
}
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
},
@@ -212,6 +209,10 @@ class OtherRoutes {
path: RouteConstants.chatScreen,
name: 'chatScreen',
builder: (context, state) {
final uuidFromQuery = state.uri.queryParameters['uuid'];
if (uuidFromQuery != null) {
return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false);
}
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
@@ -233,7 +234,7 @@ class OtherRoutes {
final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,);
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled);
},
),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),

View File

@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -96,11 +97,34 @@ class _ChatScreenState extends State<ChatScreen> {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Scaffold(
appBar: AppBar(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
);
} else {
return const Scaffold(body: Loader());
return Scaffold(
body: const Loader(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
);
}
},
),
@@ -148,8 +172,18 @@ class _ChatScreenState extends State<ChatScreen> {
resizeToAvoidBottomInset: true,
appBar: AppBar(
titleSpacing: 0,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
backgroundColor: appTheme.gray100,
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
@@ -224,7 +258,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
@@ -235,7 +269,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
_messageDate(message.updatedAt),
@@ -284,7 +318,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
@@ -295,7 +329,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)

View File

@@ -32,8 +32,8 @@ import 'package:go_router/go_router.dart';
import '../widgets/recipes.dart';
class FeedScreen extends StatelessWidget {
const FeedScreen({super.key});
const FeedScreen({super.key, this.initialTabIndex = 0});
final int initialTabIndex;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -44,14 +44,14 @@ class FeedScreen extends StatelessWidget {
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
],
child: const FeedScreenContent(),
child: FeedScreenContent(initialTabIndex: initialTabIndex),
);
}
}
class FeedScreenContent extends StatefulWidget {
const FeedScreenContent({super.key});
const FeedScreenContent({super.key, required this.initialTabIndex});
final int initialTabIndex;
@override
State<FeedScreenContent> createState() => _FeedScreenContentState();
}
@@ -64,7 +64,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsController = TextEditingController();
late final List<String> chipsLabelList;
int _selectedChipIndex = 0;
late int _selectedChipIndex;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
final dietsScrollController = ScrollController();
@@ -72,6 +72,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsScrollController = ScrollController();
@override
void initState() {
_selectedChipIndex = widget.initialTabIndex;
chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты'];
consultantsScrollController.addListener(_onScrollConsultants);
_checkRegistrationDate(context);
@@ -88,6 +89,14 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
super.initState();
}
@override
void didUpdateWidget(FeedScreenContent oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {
_selectedChipIndex = widget.initialTabIndex;
});
}
void _onScrollConsultants() {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
}

View File

@@ -6,6 +6,7 @@ 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/routing/route_constants.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/bottom_sheet_with_blur.dart';
@@ -122,7 +123,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) {
return Scaffold(
appBar: AppBar(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.feedScreen);
},
),
),
body: DataErrorWidget(
onReload: () async {
widget.slug == null ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.slug!);
@@ -130,7 +141,20 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
);
} else {
return Scaffold(appBar: AppBar(), body: const Loader());
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.feedScreen);
},
),
),
body: const Loader(),
);
}
},
),

View File

@@ -2,14 +2,15 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/notifications/push_navigation_handler.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await AppInitializer.initialize(buildType: AppBuildType.prod);
await AppInitializer.runAppWithSentry(() => runApp(const MyApp()));
}