#222 optional bottom nav bar
This commit is contained in:
parent
37fc57f694
commit
39aebf49e2
21 changed files with 547 additions and 206 deletions
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -139,7 +139,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
return AvesColorsProvider(
|
||||
child: child!,
|
||||
);
|
||||
// return child!;
|
||||
},
|
||||
onGenerateTitle: (context) => context.l10n.appName,
|
||||
theme: Themes.lightTheme,
|
||||
|
|
|
@ -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<CollectionPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: SelectionProvider<AvesEntry>(
|
||||
child: QueryProvider(
|
||||
initialQuery: liveFilter?.query,
|
||||
child: Builder(
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () {
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return SynchronousFuture(false);
|
||||
}
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: DoubleBackPopScope(
|
||||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: ChangeNotifierProvider<CollectionLens>.value(
|
||||
value: _collection,
|
||||
child: const CollectionGrid(
|
||||
// key is expected by test driver
|
||||
key: Key('collection-grid'),
|
||||
settingsRouteKey: CollectionPage.routeName,
|
||||
child: Selector<Settings, bool>(
|
||||
selector: (context, s) => s.showBottomNavigationBar,
|
||||
builder: (context, showBottomNavigationBar, child) {
|
||||
return Scaffold(
|
||||
body: SelectionProvider<AvesEntry>(
|
||||
child: QueryProvider(
|
||||
initialQuery: liveFilter?.query,
|
||||
child: Builder(
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () {
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return SynchronousFuture(false);
|
||||
}
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: DoubleBackPopScope(
|
||||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: ChangeNotifierProvider<CollectionLens>.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<CollectionPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
drawer: AppDrawer(currentCollection: _collection),
|
||||
resizeToAvoidBottomInset: false,
|
||||
drawer: AppDrawer(currentCollection: _collection),
|
||||
bottomNavigationBar: showBottomNavigationBar ? AppBottomNavBar(currentCollection: _collection) : null,
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBody: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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<T extends CollectionFilter> extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: WillPopScope(
|
||||
onWillPop: () {
|
||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return SynchronousFuture(false);
|
||||
}
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: DoubleBackPopScope(
|
||||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: FilterGrid<T>(
|
||||
// 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<Settings, bool>(
|
||||
selector: (context, s) => s.showBottomNavigationBar,
|
||||
builder: (context, showBottomNavigationBar, child) {
|
||||
return Scaffold(
|
||||
body: WillPopScope(
|
||||
onWillPop: () {
|
||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
||||
if (selection.isSelecting) {
|
||||
selection.browse();
|
||||
return SynchronousFuture(false);
|
||||
}
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: DoubleBackPopScope(
|
||||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: FilterGrid<T>(
|
||||
// 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<AppDrawer> createState() => _AppDrawerState();
|
||||
|
||||
static List<String> getDefaultAlbums(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final specialAlbums = source.rawAlbums.where((album) {
|
||||
|
@ -47,6 +50,14 @@ class AppDrawer extends StatelessWidget {
|
|||
..sort(source.compareAlbumsByName);
|
||||
return specialAlbums;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppDrawerState extends State<AppDrawer> {
|
||||
// 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),
|
|
@ -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';
|
||||
|
|
@ -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 {
|
81
lib/widgets/navigation/drawer/tile.dart
Normal file
81
lib/widgets/navigation/drawer/tile.dart
Normal file
|
@ -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));
|
||||
}
|
100
lib/widgets/navigation/nav_bar/floating.dart
Normal file
100
lib/widgets/navigation/nav_bar/floating.dart
Normal file
|
@ -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<FloatingNavBar> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _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<Offset>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
117
lib/widgets/navigation/nav_bar/nav_bar.dart
Normal file
117
lib/widgets/navigation/nav_bar/nav_bar.dart
Normal file
|
@ -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<Settings, bool>((s) => s.enableOverlayBlurEffect);
|
||||
final showVideo = context.select<Settings, bool>((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<AvesBottomNavItem> 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<AvesBottomNavItem> 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<CollectionSource>(),
|
||||
filters: {item.filter},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
36
lib/widgets/navigation/nav_bar/nav_item.dart
Normal file
36
lib/widgets/navigation/nav_bar/nav_item.dart
Normal file
|
@ -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<Object?> 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);
|
||||
}
|
||||
}
|
59
lib/widgets/navigation/nav_display.dart
Normal file
59
lib/widgets/navigation/nav_display.dart
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -31,6 +31,7 @@ class NavigationSection extends SettingsSection {
|
|||
@override
|
||||
FutureOr<List<SettingsTile>> 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;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue