diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6ecea6961..e3eaaa46e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -599,6 +599,7 @@ "settingsSectionNavigation": "Navigation", "settingsHome": "Home", + "settingsShowBottomNavigationBar": "Show bottom navigation bar", "settingsKeepScreenOnTile": "Keep screen on", "settingsKeepScreenOnTitle": "Keep Screen On", "settingsDoubleBackExit": "Tap “back” twice to exit", diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index e82f5cd8f..cbe34b9d1 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -27,6 +27,7 @@ class SettingsDefaults { static const mustBackTwiceToExit = true; static const keepScreenOn = KeepScreenOn.viewerOnly; static const homePage = HomePageSetting.collection; + static const showBottomNavigationBar = false; static const confirmDeleteForever = true; static const confirmMoveToBin = true; static const confirmMoveUndatedItems = true; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 780e24bbc..e7b25794e 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -56,6 +56,7 @@ class Settings extends ChangeNotifier { static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; + static const showBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmMoveToBinKey = 'confirm_move_to_bin'; static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items'; @@ -294,6 +295,10 @@ class Settings extends ChangeNotifier { set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); + bool get showBottomNavigationBar => getBoolOrDefault(showBottomNavigationBarKey, SettingsDefaults.showBottomNavigationBar); + + set showBottomNavigationBar(bool newValue) => setAndNotify(showBottomNavigationBarKey, newValue); + bool get confirmDeleteForever => getBoolOrDefault(confirmDeleteForeverKey, SettingsDefaults.confirmDeleteForever); set confirmDeleteForever(bool newValue) => setAndNotify(confirmDeleteForeverKey, newValue); @@ -682,6 +687,7 @@ class Settings extends ChangeNotifier { break; case isInstalledAppAccessAllowedKey: case isErrorReportingAllowedKey: + case showBottomNavigationBarKey: case mustBackTwiceToExitKey: case confirmDeleteForeverKey: case confirmMoveToBinKey: diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 7a0d5cfa5..e21d4e03c 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -139,7 +139,6 @@ class _AvesAppState extends State with WidgetsBindingObserver { return AvesColorsProvider( child: child!, ); - // return child!; }, onGenerateTitle: (context) => context.l10n.appName, theme: Themes.lightTheme, diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 405fd7daa..99087891d 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -16,7 +16,8 @@ import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; -import 'package:aves/widgets/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -74,30 +75,35 @@ class _CollectionPageState extends State { Widget build(BuildContext context) { final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return MediaQueryDataProvider( - child: Scaffold( - body: SelectionProvider( - child: QueryProvider( - initialQuery: liveFilter?.query, - child: Builder( - builder: (context) => WillPopScope( - onWillPop: () { - final selection = context.read>(); - if (selection.isSelecting) { - selection.browse(); - return SynchronousFuture(false); - } - return SynchronousFuture(true); - }, - child: DoubleBackPopScope( - child: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: ChangeNotifierProvider.value( - value: _collection, - child: const CollectionGrid( - // key is expected by test driver - key: Key('collection-grid'), - settingsRouteKey: CollectionPage.routeName, + child: Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + return Scaffold( + body: SelectionProvider( + child: QueryProvider( + initialQuery: liveFilter?.query, + child: Builder( + builder: (context) => WillPopScope( + onWillPop: () { + final selection = context.read>(); + if (selection.isSelecting) { + selection.browse(); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: DoubleBackPopScope( + child: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: ChangeNotifierProvider.value( + value: _collection, + child: const CollectionGrid( + // key is expected by test driver + key: Key('collection-grid'), + settingsRouteKey: CollectionPage.routeName, + ), + ), ), ), ), @@ -105,10 +111,12 @@ class _CollectionPageState extends State { ), ), ), - ), - ), - drawer: AppDrawer(currentCollection: _collection), - resizeToAvoidBottomInset: false, + drawer: AppDrawer(currentCollection: _collection), + bottomNavigationBar: showBottomNavigationBar ? AppBottomNavBar(currentCollection: _collection) : null, + resizeToAvoidBottomInset: false, + extendBody: true, + ); + }, ), ); } diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart deleted file mode 100644 index fa59ff537..000000000 --- a/lib/widgets/drawer/tile.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:aves/model/filters/favourite.dart'; -import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/filters/type.dart'; -import 'package:aves/theme/colors.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/debug/app_debug_page.dart'; -import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/filter_grids/countries_page.dart'; -import 'package:aves/widgets/filter_grids/tags_page.dart'; -import 'package:flutter/material.dart'; - -class DrawerFilterIcon extends StatelessWidget { - final CollectionFilter? filter; - - const DrawerFilterIcon({ - Key? key, - required this.filter, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final iconSize = 24 * textScaleFactor; - - final _filter = filter; - if (_filter == null) return Icon(AIcons.allCollection, size: iconSize); - return _filter.iconBuilder(context, iconSize) ?? const SizedBox(); - } -} - -class DrawerFilterTitle extends StatelessWidget { - final CollectionFilter? filter; - - const DrawerFilterTitle({ - Key? key, - required this.filter, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - String _getString(CollectionFilter? filter) { - final l10n = context.l10n; - if (filter == null) return l10n.drawerCollectionAll; - if (filter == FavouriteFilter.instance) return l10n.drawerCollectionFavourites; - if (filter == MimeFilter.image) return l10n.drawerCollectionImages; - if (filter == MimeFilter.video) return l10n.drawerCollectionVideos; - if (filter == TypeFilter.animated) return l10n.drawerCollectionAnimated; - if (filter == TypeFilter.motionPhoto) return l10n.drawerCollectionMotionPhotos; - if (filter == TypeFilter.panorama) return l10n.drawerCollectionPanoramas; - if (filter == TypeFilter.raw) return l10n.drawerCollectionRaws; - if (filter == TypeFilter.sphericalVideo) return l10n.drawerCollectionSphericalVideos; - return filter.getLabel(context); - } - - return Text(_getString(filter)); - } -} - -class DrawerPageIcon extends StatelessWidget { - final String route; - - const DrawerPageIcon({ - Key? key, - required this.route, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - switch (route) { - case AlbumListPage.routeName: - return const Icon(AIcons.album); - case CountryListPage.routeName: - return const Icon(AIcons.location); - case TagListPage.routeName: - return const Icon(AIcons.tag); - case AppDebugPage.routeName: - return ShaderMask( - shaderCallback: AvesColorsData.debugGradient.createShader, - blendMode: BlendMode.srcIn, - child: const Icon(AIcons.debug), - ); - default: - return const SizedBox(); - } - } -} - -class DrawerPageTitle extends StatelessWidget { - final String route; - - const DrawerPageTitle({ - Key? key, - required this.route, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - String _getString() { - final l10n = context.l10n; - switch (route) { - case AlbumListPage.routeName: - return l10n.albumPageTitle; - case CountryListPage.routeName: - return l10n.countryPageTitle; - case TagListPage.routeName: - return l10n.tagPageTitle; - case AppDebugPage.routeName: - return 'Debug'; - default: - return route; - } - } - - return Text(_getString()); - } -} diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index cfe3cd07c..12f72c669 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -21,13 +21,14 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; -import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart'; import 'package:aves/widgets/filter_grids/common/filter_tile.dart'; import 'package:aves/widgets/filter_grids/common/list_details_theme.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart'; +import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -69,42 +70,49 @@ class FilterGridPage extends StatelessWidget { @override Widget build(BuildContext context) { return MediaQueryDataProvider( - child: Scaffold( - body: WillPopScope( - onWillPop: () { - final selection = context.read>>(); - if (selection.isSelecting) { - selection.browse(); - return SynchronousFuture(false); - } - return SynchronousFuture(true); - }, - child: DoubleBackPopScope( - child: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: FilterGrid( - // key is expected by test driver - key: const Key('filter-grid'), - settingsRouteKey: settingsRouteKey, - appBar: appBar, - appBarHeight: appBarHeight, - sections: sections, - newFilters: newFilters, - sortFactor: sortFactor, - showHeaders: showHeaders, - selectable: selectable, - queryNotifier: queryNotifier, - applyQuery: applyQuery, - emptyBuilder: emptyBuilder, - heroType: heroType, + child: Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + return Scaffold( + body: WillPopScope( + onWillPop: () { + final selection = context.read>>(); + if (selection.isSelecting) { + selection.browse(); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: DoubleBackPopScope( + child: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: FilterGrid( + // key is expected by test driver + key: const Key('filter-grid'), + settingsRouteKey: settingsRouteKey, + appBar: appBar, + appBarHeight: appBarHeight, + sections: sections, + newFilters: newFilters, + sortFactor: sortFactor, + showHeaders: showHeaders, + selectable: selectable, + queryNotifier: queryNotifier, + applyQuery: applyQuery, + emptyBuilder: emptyBuilder, + heroType: heroType, + ), + ), ), ), ), - ), - ), - drawer: const AppDrawer(), - resizeToAvoidBottomInset: false, + drawer: const AppDrawer(), + bottomNavigationBar: showBottomNavigationBar ? const AppBottomNavBar() : null, + resizeToAvoidBottomInset: false, + extendBody: true, + ); + }, ), ); } diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart similarity index 94% rename from lib/widgets/drawer/app_drawer.dart rename to lib/widgets/navigation/drawer/app_drawer.dart index ef0d7512b..ba64657a7 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -17,19 +17,19 @@ 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_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; -import 'package:aves/widgets/drawer/collection_nav_tile.dart'; -import 'package:aves/widgets/drawer/page_nav_tile.dart'; -import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/navigation/drawer/collection_nav_tile.dart'; +import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/settings_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class AppDrawer extends StatelessWidget { +class AppDrawer extends StatefulWidget { // collection loaded in the `CollectionPage`, if any final CollectionLens? currentCollection; @@ -38,6 +38,9 @@ class AppDrawer extends StatelessWidget { this.currentCollection, }) : super(key: key); + @override + State createState() => _AppDrawerState(); + static List getDefaultAlbums(BuildContext context) { final source = context.read(); final specialAlbums = source.rawAlbums.where((album) { @@ -47,6 +50,14 @@ class AppDrawer extends StatelessWidget { ..sort(source.compareAlbumsByName); return specialAlbums; } +} + +class _AppDrawerState extends State { + // using the default controller conflicts + // with bottom nav bar primary scroll monitoring + final ScrollController _scrollController = ScrollController(); + + CollectionLens? get currentCollection => widget.currentCollection; @override Widget build(BuildContext context) { @@ -73,6 +84,7 @@ class AppDrawer extends StatelessWidget { builder: (context, mqPaddingBottom, child) { final iconTheme = IconTheme.of(context); return SingleChildScrollView( + controller: _scrollController, // key is expected by test driver key: const Key('drawer-scrollview'), padding: EdgeInsets.only(bottom: mqPaddingBottom), diff --git a/lib/widgets/drawer/collection_nav_tile.dart b/lib/widgets/navigation/drawer/collection_nav_tile.dart similarity index 98% rename from lib/widgets/drawer/collection_nav_tile.dart rename to lib/widgets/navigation/drawer/collection_nav_tile.dart index 8a8a73603..42642d9ef 100644 --- a/lib/widgets/drawer/collection_nav_tile.dart +++ b/lib/widgets/navigation/drawer/collection_nav_tile.dart @@ -5,7 +5,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/drawer/tile.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/drawer/page_nav_tile.dart b/lib/widgets/navigation/drawer/page_nav_tile.dart similarity index 96% rename from lib/widgets/drawer/page_nav_tile.dart rename to lib/widgets/navigation/drawer/page_nav_tile.dart index 938b43367..319462f63 100644 --- a/lib/widgets/drawer/page_nav_tile.dart +++ b/lib/widgets/navigation/drawer/page_nav_tile.dart @@ -1,5 +1,5 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/drawer/tile.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:flutter/material.dart'; class PageNavTile extends StatelessWidget { diff --git a/lib/widgets/navigation/drawer/tile.dart b/lib/widgets/navigation/drawer/tile.dart new file mode 100644 index 000000000..7294a4034 --- /dev/null +++ b/lib/widgets/navigation/drawer/tile.dart @@ -0,0 +1,81 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/colors.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/debug/app_debug_page.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/navigation/nav_display.dart'; +import 'package:flutter/material.dart'; + +class DrawerFilterIcon extends StatelessWidget { + final CollectionFilter? filter; + + const DrawerFilterIcon({ + Key? key, + required this.filter, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final iconSize = 24 * textScaleFactor; + + final _filter = filter; + if (_filter == null) return Icon(AIcons.allCollection, size: iconSize); + return _filter.iconBuilder(context, iconSize) ?? const SizedBox(); + } +} + +class DrawerFilterTitle extends StatelessWidget { + final CollectionFilter? filter; + + const DrawerFilterTitle({ + Key? key, + required this.filter, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Text(NavigationDisplay.getFilterTitle(context, filter)); +} + +class DrawerPageIcon extends StatelessWidget { + final String route; + + const DrawerPageIcon({ + Key? key, + required this.route, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final icon = NavigationDisplay.getPageIcon(route); + if (icon != null) { + switch (route) { + case AlbumListPage.routeName: + case CountryListPage.routeName: + case TagListPage.routeName: + return Icon(icon); + case AppDebugPage.routeName: + return ShaderMask( + shaderCallback: AvesColorsData.debugGradient.createShader, + blendMode: BlendMode.srcIn, + child: Icon(icon), + ); + } + } + return const SizedBox(); + } +} + +class DrawerPageTitle extends StatelessWidget { + final String route; + + const DrawerPageTitle({ + Key? key, + required this.route, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Text(NavigationDisplay.getPageTitle(context, route)); +} diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart new file mode 100644 index 000000000..6e82c61ba --- /dev/null +++ b/lib/widgets/navigation/nav_bar/floating.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; + +class FloatingNavBar extends StatefulWidget { + final ScrollController? scrollController; + final Widget child; + + const FloatingNavBar({ + Key? key, + required this.scrollController, + required this.child, + }) : super(key: key); + + @override + _FloatingNavBarState createState() => _FloatingNavBarState(); +} + +class _FloatingNavBarState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + double? _lastOffset; + double _delta = 0; + + static const double _deltaThreshold = 50; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _offsetAnimation = Tween( + begin: const Offset(0, 0), + end: const Offset(0, 1), + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.linear, + )) + ..addListener(() { + if (mounted) { + setState(() {}); + } + }); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant FloatingNavBar oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.scrollController != widget.scrollController) { + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(FloatingNavBar widget) { + _lastOffset = null; + _delta = 0; + widget.scrollController?.addListener(_onScrollChange); + } + + void _unregisterWidget(FloatingNavBar widget) { + widget.scrollController?.removeListener(_onScrollChange); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _offsetAnimation, + child: widget.child, + ); + } + + void _onScrollChange() { + final scrollController = widget.scrollController; + if (scrollController == null) return; + + final offset = scrollController.offset; + _delta += offset - (_lastOffset ?? offset); + _lastOffset = offset; + + if (_delta.abs() > _deltaThreshold) { + if (_delta > 0) { + // hide + _controller.forward(); + } else { + // show + _controller.reverse(); + } + _delta = 0; + } + } +} diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart new file mode 100644 index 000000000..d6ff8a2e7 --- /dev/null +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -0,0 +1,117 @@ +import 'package:aves/model/filters/favourite.dart'; +import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/navigation/nav_bar/floating.dart'; +import 'package:aves/widgets/navigation/nav_bar/nav_item.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AppBottomNavBar extends StatelessWidget { + // collection loaded in the `CollectionPage`, if any + final CollectionLens? currentCollection; + + const AppBottomNavBar({ + Key? key, + this.currentCollection, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + const borderRadius = BorderRadius.all(Radius.circular(8)); + + final blurred = context.select((s) => s.enableOverlayBlurEffect); + final showVideo = context.select((s) => !s.hiddenFilters.contains(MimeFilter.video)); + + final items = [ + const AvesBottomNavItem(route: CollectionPage.routeName), + if (showVideo) AvesBottomNavItem(route: CollectionPage.routeName, filter: MimeFilter.video), + const AvesBottomNavItem(route: CollectionPage.routeName, filter: FavouriteFilter.instance), + const AvesBottomNavItem(route: AlbumListPage.routeName), + ]; + + Widget child = Padding( + padding: const EdgeInsets.all(8), + child: BlurredRRect( + enabled: blurred, + borderRadius: borderRadius, + child: BottomNavigationBar( + items: items + .map((item) => BottomNavigationBarItem( + icon: item.icon(context), + label: item.label(context), + )) + .toList(), + onTap: (index) => _goTo(context, items, index), + currentIndex: _getCurrentIndex(context, items), + type: BottomNavigationBarType.fixed, + backgroundColor: Theme.of(context).canvasColor.withOpacity(.85), + showSelectedLabels: false, + showUnselectedLabels: false, + ), + ), + ); + + return Hero( + tag: 'nav-bar', + flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { + return MediaQuery.removeViewInsets( + context: context, + removeBottom: true, + child: toHero.widget, + ); + }, + child: FloatingNavBar( + scrollController: PrimaryScrollController.of(context), + child: SafeArea( + child: child, + ), + ), + ); + } + + int _getCurrentIndex(BuildContext context, List items) { + final currentRoute = context.currentRouteName; + final currentItem = items.firstWhereOrNull((item) { + if (currentRoute != item.route) return false; + + if (item.route != CollectionPage.routeName) return true; + + final currentFilters = currentCollection?.filters; + if (currentFilters == null || currentFilters.length > 1) return false; + return currentFilters.firstOrNull == item.filter; + }); + final currentIndex = currentItem != null ? items.indexOf(currentItem) : 0; + return currentIndex; + } + + void _goTo(BuildContext context, List items, int index) { + final item = items[index]; + final routeName = item.route; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: (context) { + switch (routeName) { + case AlbumListPage.routeName: + return const AlbumListPage(); + case CollectionPage.routeName: + default: + return CollectionPage( + source: context.read(), + filters: {item.filter}, + ); + } + }, + ), + (route) => false, + ); + } +} diff --git a/lib/widgets/navigation/nav_bar/nav_item.dart b/lib/widgets/navigation/nav_bar/nav_item.dart new file mode 100644 index 000000000..fbd916396 --- /dev/null +++ b/lib/widgets/navigation/nav_bar/nav_item.dart @@ -0,0 +1,36 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; +import 'package:aves/widgets/navigation/nav_display.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class AvesBottomNavItem extends Equatable { + final String route; + final CollectionFilter? filter; + + @override + List get props => [route, filter]; + + const AvesBottomNavItem({ + required this.route, + this.filter, + }); + + Widget icon(BuildContext context) { + if (route == CollectionPage.routeName) { + return DrawerFilterIcon(filter: filter); + } + + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final iconSize = 24 * textScaleFactor; + return Icon(NavigationDisplay.getPageIcon(route), size: iconSize); + } + + String label(BuildContext context) { + if (route == CollectionPage.routeName) { + return NavigationDisplay.getFilterTitle(context, filter); + } + return NavigationDisplay.getPageTitle(context, route); + } +} diff --git a/lib/widgets/navigation/nav_display.dart b/lib/widgets/navigation/nav_display.dart new file mode 100644 index 000000000..6edc5971b --- /dev/null +++ b/lib/widgets/navigation/nav_display.dart @@ -0,0 +1,59 @@ +import 'package:aves/model/filters/favourite.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/filters/type.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/debug/app_debug_page.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class NavigationDisplay { + static String getFilterTitle(BuildContext context, CollectionFilter? filter) { + final l10n = context.l10n; + if (filter == null) return l10n.drawerCollectionAll; + if (filter == FavouriteFilter.instance) return l10n.drawerCollectionFavourites; + if (filter == MimeFilter.image) return l10n.drawerCollectionImages; + if (filter == MimeFilter.video) return l10n.drawerCollectionVideos; + if (filter == TypeFilter.animated) return l10n.drawerCollectionAnimated; + if (filter == TypeFilter.motionPhoto) return l10n.drawerCollectionMotionPhotos; + if (filter == TypeFilter.panorama) return l10n.drawerCollectionPanoramas; + if (filter == TypeFilter.raw) return l10n.drawerCollectionRaws; + if (filter == TypeFilter.sphericalVideo) return l10n.drawerCollectionSphericalVideos; + return filter.getLabel(context); + } + + static String getPageTitle(BuildContext context, route) { + final l10n = context.l10n; + switch (route) { + case AlbumListPage.routeName: + return l10n.albumPageTitle; + case CountryListPage.routeName: + return l10n.countryPageTitle; + case TagListPage.routeName: + return l10n.tagPageTitle; + case AppDebugPage.routeName: + return 'Debug'; + default: + return route; + } + } + + static IconData? getPageIcon(String route) { + switch (route) { + case AlbumListPage.routeName: + return AIcons.album; + case CountryListPage.routeName: + return AIcons.location; + case TagListPage.routeName: + return AIcons.tag; + case AppDebugPage.routeName: + return AIcons.debug; + default: + return null; + } + } +} diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index d83069dc3..909b7c689 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -31,6 +31,7 @@ class DisplaySection extends SettingsSection { SettingsTileDisplayThemeBrightness(), SettingsTileDisplayThemeColorMode(), SettingsTileDisplayDisplayRefreshRateMode(), + SettingsTileDisplayEnableBlurEffect(), ]; } @@ -75,3 +76,15 @@ class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { dialogTitle: context.l10n.settingsDisplayRefreshRateModeTitle, ); } + +class SettingsTileDisplayEnableBlurEffect extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerEnableOverlayBlurEffect; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableOverlayBlurEffect, + onChanged: (v) => settings.enableOverlayBlurEffect = v, + title: title(context), + ); +} diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index cca5decaf..658b4357d 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -1,11 +1,11 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/drawer/app_drawer.dart'; -import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/settings/navigation/drawer_tab_albums.dart'; import 'package:aves/widgets/settings/navigation/drawer_tab_fixed.dart'; diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart index 187d542f8..89a8780fe 100644 --- a/lib/widgets/settings/navigation/drawer_tab_albums.dart +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -3,8 +3,8 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons.dart'; -import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index fa791b225..ecbd33b57 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -31,6 +31,7 @@ class NavigationSection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ SettingsTileNavigationHomePage(), + SettingsTileShowBottomNavigationBar(), SettingsTileNavigationDrawer(), SettingsTileNavigationConfirmationDialog(), SettingsTileNavigationKeepScreenOn(), @@ -53,6 +54,18 @@ class SettingsTileNavigationHomePage extends SettingsTile { ); } +class SettingsTileShowBottomNavigationBar extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsShowBottomNavigationBar; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.showBottomNavigationBar, + onChanged: (v) => settings.showBottomNavigationBar = v, + title: title(context), + ); +} + class SettingsTileNavigationDrawer extends SettingsTile { @override String title(BuildContext context) => context.l10n.settingsNavigationDrawerTile; diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index 66b1c4e41..95342d7d5 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -52,11 +52,6 @@ class ViewerOverlayPage extends StatelessWidget { onChanged: (v) => settings.showOverlayThumbnailPreview = v, title: context.l10n.settingsViewerShowOverlayThumbnails, ), - SettingsSwitchListTile( - selector: (context, s) => s.enableOverlayBlurEffect, - onChanged: (v) => settings.enableOverlayBlurEffect = v, - title: context.l10n.settingsViewerEnableOverlayBlurEffect, - ), ], ), ), diff --git a/untranslated.json b/untranslated.json index c044f2945..eae752520 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,7 +1,8 @@ { "de": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "es": [ @@ -16,22 +17,26 @@ "appPickDialogTitle", "appPickDialogNone", "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "fr": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "id": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "it": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "ja": [ @@ -46,26 +51,31 @@ "appPickDialogTitle", "appPickDialogNone", "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "ko": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "pt": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "ru": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ], "zh": [ "settingsSearchFieldLabel", - "settingsSearchEmpty" + "settingsSearchEmpty", + "settingsShowBottomNavigationBar" ] }