mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-19 07:09:14 +00:00
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:
parent
ea2cc28ac9
commit
1e75dbcb81
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()],
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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]);
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue