feat: root_scaffold_with_subroutes

rewrote root_scaffold_with_navigation:
* extracted common code
* removed dead one
* cleaned up remaining one
* fixed translations update on language change
This commit is contained in:
Aliaksei Tratseuski 2024-05-15 19:45:04 +04:00
parent ea2cc28ac9
commit 1e75dbcb81
8 changed files with 284 additions and 300 deletions

View file

@ -1,295 +0,0 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
class RootScaffoldWithNavigation extends StatelessWidget {
const RootScaffoldWithNavigation({
required this.child,
required this.title,
required this.destinations,
this.showBottomBar = true,
this.showFab = true,
super.key,
});
final Widget child;
final String title;
final bool showBottomBar;
final List<RouteDestination> destinations;
final bool showFab;
@override
// ignore: prefer_expression_function_bodies
Widget build(final BuildContext context) {
return Scaffold(
appBar: Breakpoints.mediumAndUp.isActive(context)
? PreferredSize(
preferredSize: const Size.fromHeight(52),
child: _RootAppBar(title: title),
)
: null,
endDrawer: const SupportDrawer(),
endDrawerEnableOpenDragGesture: false,
body: Row(
children: [
if (Breakpoints.medium.isActive(context))
_MainScreenNavigationRail(
destinations: destinations,
showFab: showFab,
),
if (Breakpoints.large.isActive(context))
_MainScreenNavigationDrawer(
destinations: destinations,
showFab: showFab,
),
Expanded(child: child),
],
),
bottomNavigationBar: _BottomBar(
destinations: destinations,
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
key: const Key('bottomBar'),
),
floatingActionButton:
showFab && Breakpoints.small.isActive(context) && showBottomBar
? const BrandFab()
: null,
);
}
}
class _RootAppBar extends StatelessWidget {
const _RootAppBar({
required this.title,
});
final String title;
@override
Widget build(final BuildContext context) => AppBar(
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder:
(final Widget child, final Animation<double> animation) =>
SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
),
child: SizedBox(
key: ValueKey<String>(title),
width: double.infinity,
child: Text(
title,
),
),
),
leading: context.router.pageCount > 1
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.router.maybePop(),
)
: null,
actions: const [
SizedBox.shrink(),
],
);
}
class _MainScreenNavigationRail extends StatelessWidget {
const _MainScreenNavigationRail({
required this.destinations,
this.showFab = true,
});
final List<RouteDestination> destinations;
final bool showFab;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final prevActiveIndex = destinations.indexWhere(
(final destination) => context.router.stack
.any((final route) => route.name == destination.route.routeName),
);
if (activeIndex == -1) {
if (prevActiveIndex != -1) {
activeIndex = prevActiveIndex;
} else {
activeIndex = 0;
}
}
final isExtended = Breakpoints.large.isActive(context);
return LayoutBuilder(
builder: (final context, final constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
labelType: isExtended
? NavigationRailLabelType.none
: NavigationRailLabelType.all,
extended: isExtended,
leading: showFab
? const BrandFab(
extended: false,
)
: null,
groupAlignment: 0.0,
destinations: destinations
.map(
(final destination) => NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
)
.toList(),
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
),
),
),
),
);
}
}
class _BottomBar extends StatelessWidget {
const _BottomBar({
required this.destinations,
required this.hidden,
super.key,
});
final List<RouteDestination> destinations;
final bool hidden;
@override
Widget build(final BuildContext context) {
final prevActiveIndex = destinations.indexWhere(
(final destination) => context.router.stack
.any((final route) => route.name == destination.route.routeName),
);
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: hidden ? 0 : 80,
curve: Curves.easeInOutCubicEmphasized,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Platform.isIOS
? CupertinoTabBar(
currentIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex,
onTap: (final index) {
context.router.replaceAll([destinations[index].route]);
},
items: destinations
.map(
(final destination) => BottomNavigationBarItem(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(),
)
: NavigationBar(
selectedIndex: prevActiveIndex == -1 ? 0 : prevActiveIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
destinations: destinations
.map(
(final destination) => NavigationDestination(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(),
),
);
}
}
class _MainScreenNavigationDrawer extends StatelessWidget {
const _MainScreenNavigationDrawer({
required this.destinations,
this.showFab = true,
});
final List<RouteDestination> destinations;
final bool showFab;
@override
Widget build(final BuildContext context) {
int? activeIndex = destinations.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final prevActiveIndex = destinations.indexWhere(
(final destination) => context.router.stack
.any((final route) => route.name == destination.route.routeName),
);
if (activeIndex == -1) {
if (prevActiveIndex != -1) {
activeIndex = prevActiveIndex;
} else {
activeIndex = 0;
}
}
return SizedBox(
height: MediaQuery.of(context).size.height,
width: 296,
child: NavigationDrawer(
key: const Key('PrimaryNavigationDrawer'),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
surfaceTintColor: Colors.transparent,
selectedIndex: activeIndex,
onDestinationSelected: (final index) {
context.router.replaceAll([destinations[index].route]);
},
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: BrandFab(extended: true),
),
const SizedBox(height: 16),
...destinations.map(
(final destination) => NavigationDrawerDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
),
],
),
);
}
}

View file

@ -0,0 +1,50 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _BottomTabBar extends SubrouteSelector {
const _BottomTabBar({
required super.subroutes,
required this.hidden,
super.key,
});
final bool hidden;
@override
Widget build(final BuildContext context) {
final int activeIndex = getActiveIndex(context);
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: hidden ? 0 : 80,
curve: Curves.easeInOutCubicEmphasized,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Platform.isIOS
? CupertinoTabBar(
currentIndex: activeIndex,
onTap: openSubpage(context),
items: [
for (final destination in subroutes)
BottomNavigationBarItem(
icon: Icon(destination.icon),
label: destination.label.tr(),
),
],
)
: NavigationBar(
selectedIndex: activeIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
onDestinationSelected: openSubpage(context),
destinations: [
for (final destination in subroutes)
NavigationDestination(
icon: Icon(destination.icon),
label: destination.label.tr(),
),
].toList(),
),
);
}
}

View file

@ -0,0 +1,35 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _NavigationDrawer extends SubrouteSelector {
const _NavigationDrawer({
required super.subroutes,
this.showFab = true,
});
final bool showFab;
@override
Widget build(final BuildContext context) => SizedBox(
height: MediaQuery.of(context).size.height,
width: 296,
child: NavigationDrawer(
key: const Key('PrimaryNavigationDrawer'),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
surfaceTintColor: Colors.transparent,
selectedIndex: getActiveIndex(context),
onDestinationSelected: openSubpage(context),
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: BrandFab(extended: true),
),
const SizedBox(height: 16),
for (final destination in subroutes)
NavigationDrawerDestination(
icon: Icon(destination.icon),
label: Text(destination.label.tr()),
),
],
),
);
}

View file

@ -0,0 +1,47 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _NavigationRail extends SubrouteSelector {
const _NavigationRail({
required super.subroutes,
this.showFab = true,
});
final bool showFab;
@override
Widget build(final BuildContext context) {
final isExtended = Breakpoints.large.isActive(context);
return LayoutBuilder(
builder: (final context, final constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
labelType: isExtended
? NavigationRailLabelType.none
: NavigationRailLabelType.all,
extended: isExtended,
leading: showFab
? const BrandFab(
extended: false,
)
: null,
groupAlignment: 0.0,
destinations: [
for (final destination in subroutes)
NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label.tr()),
),
],
selectedIndex: getActiveIndex(context),
onDestinationSelected: openSubpage(context),
),
),
),
),
);
}
}

View file

@ -0,0 +1,50 @@
part of 'root_scaffold_with_subroute_selector.dart';
class _RootAppBar extends StatelessWidget implements PreferredSizeWidget {
const _RootAppBar({
required this.title,
});
final String title;
@override
Size get preferredSize => const Size.fromHeight(52);
@override
Widget build(final BuildContext context) => AppBar(
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder:
(final Widget child, final Animation<double> animation) =>
SlideTransition(
position: animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.2),
end: Offset.zero,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
),
child: SizedBox(
key: ValueKey<String>(title),
width: double.infinity,
child: Text(
title,
maxLines: 1,
textAlign: TextAlign.start,
overflow: TextOverflow.fade,
),
),
),
leading: context.router.pageCount > 1
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.router.maybePop(),
)
: null,
actions: const [SizedBox.shrink()],
);
}

View file

@ -0,0 +1,67 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
part 'bottom_tab_bar.dart';
part 'navigation_drawer.dart';
part 'navigation_rail.dart';
part 'root_app_bar.dart';
part 'subroute_selector.dart';
class RootScaffoldWithSubrouteSelector extends StatelessWidget {
const RootScaffoldWithSubrouteSelector({
required this.child,
required this.destinations,
this.showBottomBar = true,
this.showFab = true,
super.key,
});
final Widget child;
final bool showBottomBar;
final List<RouteDestination> destinations;
final bool showFab;
@override
Widget build(final BuildContext context) => Scaffold(
appBar: Breakpoints.mediumAndUp.isActive(context)
? _RootAppBar(
title: getRouteTitle(context.router.current.name).tr(),
)
: null,
endDrawer: const SupportDrawer(),
endDrawerEnableOpenDragGesture: false,
body: Row(
children: [
if (Breakpoints.medium.isActive(context))
_NavigationRail(
subroutes: destinations,
showFab: showFab,
)
else if (Breakpoints.large.isActive(context))
_NavigationDrawer(
subroutes: destinations,
showFab: showFab,
),
Expanded(child: child),
],
),
bottomNavigationBar: _BottomTabBar(
key: const ValueKey('bottomBar'),
subroutes: destinations,
hidden: !(Breakpoints.small.isActive(context) && showBottomBar),
),
floatingActionButton:
showFab && Breakpoints.small.isActive(context) && showBottomBar
? const BrandFab()
: null,
);
}

View file

@ -0,0 +1,33 @@
part of 'root_scaffold_with_subroute_selector.dart';
abstract class SubrouteSelector extends StatelessWidget {
const SubrouteSelector({
required this.subroutes,
super.key,
});
final List<RouteDestination> subroutes;
int getActiveIndex(final BuildContext context) {
int activeIndex = subroutes.indexWhere(
(final destination) =>
context.router.isRouteActive(destination.route.routeName),
);
final prevActiveIndex = subroutes.indexWhere(
(final destination) => context.router.stack.any(
(final route) => route.name == destination.route.routeName,
),
);
if (activeIndex == -1) {
activeIndex = prevActiveIndex != -1 ? prevActiveIndex : 0;
}
return activeIndex;
}
ValueSetter<int> openSubpage(final BuildContext context) => (final index) {
context.router.replaceAll([subroutes[index].route]);
};
}

View file

@ -1,9 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/app_controller/inherited_app_controller.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart';
import 'package:selfprivacy/ui/layouts/root_scaffold_with_subroute_selector/root_scaffold_with_subroute_selector.dart';
import 'package:selfprivacy/ui/router/root_destinations.dart';
import 'package:selfprivacy/ui/router/router.dart';
@ -42,9 +41,7 @@ class _RootPageState extends State<RootPage> with TickerProviderStateMixin {
final isOtherRouterActive =
context.router.root.current.name != RootRoute.name;
final routeName = getRouteTitle(context.router.current.name).tr();
return RootScaffoldWithNavigation(
title: routeName,
return RootScaffoldWithSubrouteSelector(
destinations: rootDestinations,
showBottomBar:
!(currentDestinationIndex == -1 && !isOtherRouterActive),