feat(notifications): added navigation from different types of notifications

This commit is contained in:
misha
2026-02-18 19:45:32 +03:00
parent bb97c395dd
commit c540cbe6df
13 changed files with 974 additions and 507 deletions

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
@@ -31,8 +32,8 @@ class MainNavigationRoutes {
profileAvatarCubit.getMyProfile();
});
}
return BottomNavigationBarWidget(child: child);
return _MainShellWithNavScope(child: child);
},
routes: [
GoRoute(
@@ -58,7 +59,7 @@ class MainNavigationRoutes {
GoRoute(
path: RouteConstants.diaryScreen,
name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)),
),
GoRoute(
path: ConsultationsScreen.path,
@@ -68,7 +69,10 @@ class MainNavigationRoutes {
GoRoute(
path: RouteConstants.chatSelectScreen,
name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
pageBuilder: (context, state) {
final initialSearchQuery = state.extra is String ? state.extra as String : null;
return NoTransitionPage(child: ChatsScreen(initialSearchQuery: initialSearchQuery));
},
),
GoRoute(
path: RouteConstants.notificationsScreen,
@@ -94,3 +98,26 @@ 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});
final Widget child;
@override
State<_MainShellWithNavScope> createState() => _MainShellWithNavScopeState();
}
class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
final BottomNavController _navController = BottomNavController();
@override
Widget build(BuildContext context) {
return BottomNavScope(
controller: _navController,
child: BottomNavigationBarWidget(child: widget.child),
);
}
}

View File

@@ -195,7 +195,7 @@ class OtherRoutes {
GoRoute(
path: '${RouteConstants.subsScreen}/:slug',
name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug'], subType: state.extra as SubType),
),
// Chat routes
@@ -276,8 +276,7 @@ class OtherRoutes {
builder: (context, state) {
final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
return UserDiaryScreen(diaryId: diaryId);
},
),
GoRoute(

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
class BottomNavController {
BottomNavController();
void Function(String path, Object? extra)? _onSwitchToRoute;
void register(void Function(String path, Object? extra) callback) {
_onSwitchToRoute = callback;
}
void switchToRoute(String path, [Object? extra]) {
_onSwitchToRoute?.call(path, extra);
}
}
class BottomNavScope extends InheritedWidget {
const BottomNavScope({
required this.controller,
required super.child,
super.key,
});
final BottomNavController controller;
static BottomNavController? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<BottomNavScope>()?.controller;
}
static BottomNavController of(BuildContext context) {
final controller = maybeOf(context);
assert(controller != null, 'No BottomNavScope found in context');
return controller!;
}
@override
bool updateShouldNotify(BottomNavScope oldWidget) => controller != oldWidget.controller;
}

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -66,6 +67,24 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
}
}
/// Switch to the tab that corresponds to [path], optionally with [extra]
/// (e.g. date for diary). If [path] is not in the current tab list, falls back to push.
void _switchToRoute(String path, [Object? extra]) {
if (pages == null) return;
final index = pages!.indexOf(path);
if (index >= 0 && index != currentBottomNavBarIndex) {
setState(() {
currentBottomNavBarIndex = index;
});
GoRouter.of(context).replace(path, extra: extra);
} else if (index >= 0) {
// Same tab but with new extra (e.g. diary with date)
GoRouter.of(context).replace(path, extra: extra);
} else {
GoRouter.of(context).push(path, extra: extra);
}
}
static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500
Widget _buildLogoIcon() {
@@ -121,6 +140,12 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
// Инициализируем pages на основе profileModel
pages ??= _getPages(isConsultant);
// Expose tab switching to descendants (e.g. notification widget)
final controller = BottomNavScope.maybeOf(context);
if (controller != null) {
controller.register(_switchToRoute);
}
return Scaffold(
body: widget.child,
bottomNavigationBar: BottomNavigationBar(

View File

@@ -11,7 +11,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ChatsScreen extends StatefulWidget {
const ChatsScreen({super.key});
const ChatsScreen({super.key, this.initialSearchQuery});
/// When set (e.g. from notification tap), the search field is pre-filled
/// and chat list is filtered by this query.
final String? initialSearchQuery;
@override
State<ChatsScreen> createState() => _ChatsScreenState();
@@ -23,12 +27,17 @@ class _ChatsScreenState extends State<ChatsScreen> {
@override
void initState() {
super.initState();
// Инициализация ChatsCubit при первом открытии экрана
final query = widget.initialSearchQuery?.trim();
if (query != null && query.isNotEmpty) {
searchController.text = query;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final cubit = context.read<ChatsCubit>();
final state = cubit.state;
if (state.state == BaseState.initial) {
cubit.getChatRoomsWithLoading();
cubit.getChatRoomsWithLoading(query: query?.isNotEmpty == true ? query : null);
} else if (query != null && query.isNotEmpty) {
cubit.getChatRooms(query: query);
}
});
}

View File

@@ -172,8 +172,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
);
return;
}
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1});
},
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id,});
},
borderRadius: BorderRadius.circular(100),
child: Ink(
decoration: BoxDecoration(
@@ -255,7 +255,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
.where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished)
.lastOrNull;
if (lastActiveDay != null) {
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1});
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id,});
return;
}
showErrorDialog(

View File

@@ -48,6 +48,11 @@ class DiaryCubit extends Cubit<DiaryState> {
String comment = '';
void setInitialDate(DateTime? date) {
if (date == null) return;
selectedDay = date;
}
Future<bool> setSelectedDay(DateTime day) async {
final prevDay = selectedDay;
final prevMeal = selectedMeal;
@@ -63,7 +68,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return canSelect;
}
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId, DateTime? date}) async {
final currentState = state;
try {
if (needLoading) {
@@ -77,6 +82,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return true;
}
}
final diaryDay = await getDiary();
if (diaryDay == null) {
emit(const DiaryState.error(errorMessage: 'Diary not found'));

View File

@@ -24,31 +24,29 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DiaryScreen extends StatelessWidget {
const DiaryScreen({super.key});
const DiaryScreen({super.key, required this.date});
final DateTime? date;
@override
Widget build(BuildContext context) {
return const DiaryScreenContent();
return DiaryScreenContent(date: date);
}
}
class UserDiaryScreen extends StatelessWidget {
static const path = '/user-diary-screen';
static const name = 'userDiaryScreen';
const UserDiaryScreen({super.key, required this.diaryId, required this.currentPlanDay});
const UserDiaryScreen({super.key, required this.diaryId});
final int diaryId;
final int currentPlanDay;
@override
Widget build(BuildContext context) {
return DiaryScreenContent(diaryId: diaryId, currentPlanDay: currentPlanDay);
return DiaryScreenContent(diaryId: diaryId,);
}
}
class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
const DiaryScreenContent({super.key, this.diaryId,this.date});
final int? diaryId;
final int? currentPlanDay;
final DateTime? date;
@override
State<DiaryScreenContent> createState() => _DiaryScreenContentState();
}
@@ -61,7 +59,11 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
final s = S.of(context);
return MultiBlocProvider(
providers: [
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)),
BlocProvider<DiaryCubit>(
create: (BuildContext context) => sl<DiaryCubit>()
..setInitialDate(widget.date)
..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId),
),
BlocProvider(create: (context) => sl<ConsultantCommentsCubit>()),
],
child: BlocBuilder<DiaryCubit, DiaryState>(
@@ -91,7 +93,7 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
physics: const BouncingScrollPhysics(),
child: Column(
children: [
if (widget.diaryId == null) const CalendarWidget(),
if (widget.diaryId == null) CalendarWidget(date: widget.date),
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
if (state is DiaryStateLoaded)
WakeupAndRetireWidget(

View File

@@ -8,7 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarWidget extends StatefulWidget {
const CalendarWidget({super.key});
const CalendarWidget({super.key, this.date});
final DateTime? date;
@override
State<CalendarWidget> createState() => _CalendarWidgetState();
@@ -18,6 +19,23 @@ class _CalendarWidgetState extends State<CalendarWidget> {
CalendarFormat _calendarFormat = CalendarFormat.week;
DateTime _focusedDay = DateTime.now();
DateTime _selectedDay = DateTime.now();
@override
void initState() {
super.initState();
if (widget.date != null) {
_selectedDay = widget.date!;
_focusedDay = widget.date!;
}
}
@override
void didUpdateWidget(CalendarWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.date != null && widget.date != oldWidget.date) {
_selectedDay = widget.date!;
_focusedDay = widget.date!;
}
}
@override
Widget build(BuildContext context) {

File diff suppressed because it is too large Load Diff

View File

@@ -5,22 +5,30 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class SubCubit extends Cubit<SubState> {
final ProfileRepository profileRepository;
SubCubit({
required this.profileRepository,
}) : super(const SubState());
SubCubit({required this.profileRepository}) : super(const SubState());
Future<void> getSubscriptions(String slug, [String? query]) async {
Future<void> getSubscriptions(String? slug, [String? query]) async {
emit(const SubState.loading());
try {
if (slug == null) {
final myProfile = await profileRepository.getMyProfile();
emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(myProfile.slug, query)));
return;
}
emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(slug, query)));
} catch (e) {
emit(SubState.error(errorMessage: e.toString()));
}
}
Future<void> getSubscribers(String slug, [String? query]) async {
Future<void> getSubscribers(String? slug, [String? query]) async {
emit(const SubState.loading());
try {
if (slug == null) {
final myProfile = await profileRepository.getMyProfile();
emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(myProfile.slug, query)));
return;
}
emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(slug, query)));
} catch (e) {
emit(SubState.error(errorMessage: e.toString()));

View File

@@ -12,7 +12,7 @@ enum SubType { subscribers, subscriptions }
class SubsScreen extends StatefulWidget {
final SubType subType;
final String slug;
final String? slug;
const SubsScreen({
super.key,