// lib/widgets/navigation/nav_bar/nav_bar.dart import 'package:aves/app_mode.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/activity/activity_files_page.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/navigation/nav_bar/floating.dart'; import 'package:aves/widgets/navigation/nav_item.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; enum AppBottomNavBarMode { media, activity, } class AppBottomNavBar extends StatefulWidget { final Stream events; // collection loaded in the `CollectionPage`, if any final CollectionLens? currentCollection; final AppBottomNavBarMode mode; /// Used only when [mode] is [AppBottomNavBarMode.activity]. /// /// Mapping: /// - 0: activity collection /// - 1: sport /// - 2: favourites /// - 3: activity albums final int activityIndex; /// Used only when [mode] is [AppBottomNavBarMode.activity]. final ValueChanged? onActivityIndexSelected; static double get height => kBottomNavigationBarHeight + AvesFloatingBar.margin.vertical; const AppBottomNavBar({ super.key, required this.events, this.currentCollection, this.mode = AppBottomNavBarMode.media, this.activityIndex = 0, this.onActivityIndexSelected, }); @override State createState() => _AppBottomNavBarState(); } class _AppBottomNavBarState extends State { String? _lastRoute; @override void initState() { super.initState(); _registerWidget(widget); } @override void didUpdateWidget(covariant AppBottomNavBar oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); } @override void dispose() { _unregisterWidget(widget); super.dispose(); } void _registerWidget(AppBottomNavBar widget) { widget.currentCollection?.filterChangeNotifier.addListener( _onCollectionFilterChanged, ); } void _unregisterWidget(AppBottomNavBar widget) { widget.currentCollection?.filterChangeNotifier.removeListener( _onCollectionFilterChanged, ); } @override Widget build(BuildContext context) { final mediaItems = context.select>( (v) => v.bottomNavigationActions, ); if (mediaItems.length < 2 && widget.mode == AppBottomNavBarMode.media) { return const SizedBox(); } Widget child = FloatingNavBar( scrollController: PrimaryScrollController.of(context), events: widget.events, childHeight: AppBottomNavBar.height + context.select( (mq) => mq.effectiveBottomPadding, ), child: SafeArea( child: AvesFloatingBar( builder: (context, backgroundColor, child) { final navItems = widget.mode == AppBottomNavBarMode.activity ? _buildActivityNavItems(context) : _buildMediaNavItems(context, mediaItems); return BottomNavigationBar( items: navItems, onTap: (index) => _goTo(context, mediaItems, index), currentIndex: _getCurrentIndex(context, mediaItems), type: BottomNavigationBarType.fixed, backgroundColor: backgroundColor, showSelectedLabels: false, showUnselectedLabels: false, ); }, ), ), ); // Important: // Do not wrap the Activity bottom bar in the same Hero as the media bottom bar. // Otherwise, when Activity replaces Collection, both bars can compete for the // same Hero tag and trigger GlobalKey / inactive element issues. final animate = widget.mode == AppBottomNavBarMode.media && context.select((v) => v.animate); if (animate) { child = Hero( tag: 'nav-bar', flightShuttleBuilder: ( flightContext, animation, flightDirection, fromHeroContext, toHeroContext, ) { return MediaQuery.removeViewInsets( context: context, removeBottom: true, child: toHeroContext.widget, ); }, child: child, ); } return child; } List _buildMediaNavItems( BuildContext context, List items, ) { return [ _buildToggleItem(context), ...items.map((item) { final label = item.getText(context); return BottomNavigationBarItem( icon: item.getIcon(context), label: label, tooltip: label, ); }), ]; } List _buildActivityNavItems(BuildContext context) { final scheme = Theme.of(context).colorScheme; return [ _buildToggleItem(context), // 0 - Activity collection BottomNavigationBarItem( icon: Icon( Icons.route_outlined, color: widget.activityIndex == 0 ? scheme.primary : null, ), activeIcon: Icon( Icons.route, color: scheme.primary, ), label: 'Attività', tooltip: 'Tutte le attività', ), // 1 - Sport BottomNavigationBarItem( icon: Icon( Icons.directions_run_outlined, color: widget.activityIndex == 1 ? scheme.primary : null, ), activeIcon: Icon( Icons.directions_run, color: scheme.primary, ), label: 'Sport', tooltip: 'Sport', ), // 2 - Favourite activities BottomNavigationBarItem( icon: Icon( Icons.favorite_border, color: widget.activityIndex == 2 ? scheme.primary : null, ), activeIcon: Icon( Icons.favorite, color: scheme.primary, ), label: 'Preferiti', tooltip: 'Attività preferite', ), // 3 - Activity albums BottomNavigationBarItem( icon: Icon( Icons.collections_bookmark_outlined, color: widget.activityIndex == 3 ? scheme.primary : null, ), activeIcon: Icon( Icons.collections_bookmark, color: scheme.primary, ), label: 'Album', tooltip: 'Album attività', ), ]; } BottomNavigationBarItem _buildToggleItem(BuildContext context) { final scheme = Theme.of(context).colorScheme; final isActivityMode = widget.mode == AppBottomNavBarMode.activity; final label = isActivityMode ? 'Foto' : 'Attività'; return BottomNavigationBarItem( icon: Icon( isActivityMode ? Icons.photo_library_outlined : Icons.route_outlined, color: isActivityMode ? scheme.primary : scheme.tertiary, ), activeIcon: Icon( isActivityMode ? Icons.photo_library : Icons.route, color: isActivityMode ? scheme.primary : scheme.tertiary, ), label: label, tooltip: label, ); } void _onCollectionFilterChanged() => setState(() {}); int _getCurrentIndex(BuildContext context, List mediaItems) { // Activity mode: // index 0 = toggle back to photos // index 1..4 = activity sections if (widget.mode == AppBottomNavBarMode.activity) { return widget.activityIndex + 1; } // Media mode: // index 0 = toggle to activity // index 1..N = configured media nav items // current route may be null during navigation final currentRoute = context.currentRouteName ?? _lastRoute; _lastRoute = currentRoute; final currentItem = mediaItems.firstWhereOrNull((item) { final itemRoute = item.route; if (currentRoute != itemRoute) return false; switch (itemRoute) { case CollectionPage.routeName: final currentFilters = widget.currentCollection?.filters ?? {}; return const SetEquality().equals( currentFilters, item.filters ?? {}, ); default: return true; } }); // +1 because index 0 is the activity/photo toggle. final currentIndex = currentItem != null ? mediaItems.indexOf(currentItem) + 1 : 1; return currentIndex; } void _goTo(BuildContext context, List mediaItems, int index) { if (index == 0) { _toggleActivityMode(context, mediaItems); return; } if (widget.mode == AppBottomNavBarMode.activity) { widget.onActivityIndexSelected?.call(index - 1); return; } // Media mode: // -1 because index 0 is the activity/photo toggle. final item = mediaItems[index - 1]; item.goTo(context, topLevel: null); } void _toggleActivityMode( BuildContext context, List mediaItems, ) { if (widget.mode == AppBottomNavBarMode.activity) { final collectionItem = mediaItems.firstWhereOrNull( (item) => item.route == CollectionPage.routeName, ); if (collectionItem != null) { collectionItem.goTo(context, topLevel: null); return; } // Fallback. In normal conditions this should not be used because the // default bottom nav actions always contain CollectionPage.routeName. Navigator.of(context).pushReplacement( MaterialPageRoute( settings: const RouteSettings( name: CollectionPage.routeName, ), builder: (_) => const SizedBox(), ), ); return; } // Important: // Use pushReplacement, not push, so the media Collection and its // GridItemTracker/GlobalKeys are removed while Activity is displayed. Navigator.of(context).pushReplacement( MaterialPageRoute( settings: const RouteSettings( name: ActivityFilesPage.routeName, ), builder: (_) => const ActivityFilesPage(), ), ); } } class NavBarPaddingSliver extends StatelessWidget { const NavBarPaddingSliver({super.key}); @override Widget build(BuildContext context) { final canNavigate = context.select, bool>( (v) => v.value.canNavigate, ); final enableBottomNavigationBar = context.select( (v) => v.enableBottomNavigationBar, ); final showBottomNavigationBar = canNavigate && enableBottomNavigationBar; return SliverToBoxAdapter( child: SizedBox( height: showBottomNavigationBar ? AppBottomNavBar.height : 0, ), ); } }