14 changed files with 419 additions and 293 deletions
			
			
		| @ -0,0 +1,210 @@ | ||||
| import 'dart:async'; | ||||
| 
 | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:neon/l10n/localizations.dart'; | ||||
| import 'package:neon/neon.dart'; | ||||
| import 'package:neon/src/router.dart'; | ||||
| import 'package:neon/src/widgets/drawer_destination.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| 
 | ||||
| @internal | ||||
| class NeonDrawer extends StatelessWidget { | ||||
|   const NeonDrawer({ | ||||
|     super.key, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(final BuildContext context) { | ||||
|     final accountsBloc = Provider.of<AccountsBloc>(context, listen: false); | ||||
|     final appsBloc = accountsBloc.activeAppsBloc; | ||||
| 
 | ||||
|     return StreamBuilder( | ||||
|       stream: appsBloc.appImplementations, | ||||
|       builder: (final context, final snapshot) { | ||||
|         if (snapshot.data?.data == null) { | ||||
|           return Container(); | ||||
|         } | ||||
| 
 | ||||
|         return _NeonDrawer( | ||||
|           apps: snapshot.data!.data!, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class _NeonDrawer extends StatefulWidget { | ||||
|   const _NeonDrawer({ | ||||
|     required this.apps, | ||||
|   }); | ||||
| 
 | ||||
|   final Iterable<AppImplementation> apps; | ||||
| 
 | ||||
|   @override | ||||
|   State<_NeonDrawer> createState() => __NeonDrawerState(); | ||||
| } | ||||
| 
 | ||||
| class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStateMixin { | ||||
|   late TabController _tabController; | ||||
|   late AccountsBloc _accountsBloc; | ||||
|   late AppsBloc _appsBloc; | ||||
|   late List<AppImplementation> _apps; | ||||
| 
 | ||||
|   int _activeApp = 0; | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
| 
 | ||||
|     _accountsBloc = Provider.of<AccountsBloc>(context, listen: false); | ||||
|     _appsBloc = _accountsBloc.activeAppsBloc; | ||||
| 
 | ||||
|     _apps = widget.apps.toList(); | ||||
|     _activeApp = _apps.indexWhere((final app) => app.id == _appsBloc.activeAppID.valueOrNull); | ||||
| 
 | ||||
|     _tabController = TabController( | ||||
|       vsync: this, | ||||
|       length: widget.apps.length, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _tabController.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| 
 | ||||
|   void onAppChange(final int index) { | ||||
|     Scaffold.maybeOf(context)?.closeDrawer(); | ||||
| 
 | ||||
|     // selected item is not a registered app like the SettingsPage | ||||
|     if (index >= _apps.length) { | ||||
|       const SettingsRoute().go(context); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     setState(() { | ||||
|       _activeApp = index; | ||||
|     }); | ||||
| 
 | ||||
|     unawaited(_appsBloc.setActiveApp(_apps[index].id)); | ||||
|     //context.goNamed(apps[index].routeName); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(final BuildContext context) { | ||||
|     final appDestinations = _apps.map( | ||||
|       (final app) => NavigationDrawerDestinationExtension.fromNeonDestination( | ||||
|         app.destination(context), | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     final drawer = NavigationDrawer( | ||||
|       selectedIndex: _activeApp, | ||||
|       onDestinationSelected: onAppChange, | ||||
|       children: [ | ||||
|         const NeonDrawerHeader(), | ||||
|         ...appDestinations, | ||||
|         NavigationDrawerDestination( | ||||
|           icon: const Icon(Icons.settings), | ||||
|           label: Text(AppLocalizations.of(context).settings), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
| 
 | ||||
|     return drawer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @internal | ||||
| class NeonDrawerHeader extends StatelessWidget { | ||||
|   const NeonDrawerHeader({super.key}); | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(final BuildContext context) { | ||||
|     final accountsBloc = Provider.of<AccountsBloc>(context, listen: false); | ||||
|     final capabilitiesBloc = accountsBloc.activeCapabilitiesBloc; | ||||
| 
 | ||||
|     final accountSelecor = StreamBuilder<List<Account>>( | ||||
|       stream: accountsBloc.accounts, | ||||
|       builder: (final context, final accountsSnapshot) { | ||||
|         final accounts = accountsSnapshot.data; | ||||
|         if (accounts == null || accounts.length <= 1) { | ||||
|           return const SizedBox.shrink(); | ||||
|         } | ||||
| 
 | ||||
|         final items = accounts.map((final account) { | ||||
|           final child = NeonAccountTile( | ||||
|             account: account, | ||||
|             dense: true, | ||||
|             textColor: Theme.of(context).appBarTheme.foregroundColor, | ||||
|           ); | ||||
| 
 | ||||
|           return DropdownMenuItem( | ||||
|             value: account, | ||||
|             child: child, | ||||
|           ); | ||||
|         }).toList(); | ||||
| 
 | ||||
|         return DropdownButtonHideUnderline( | ||||
|           child: DropdownButton( | ||||
|             isExpanded: true, | ||||
|             dropdownColor: Theme.of(context).colorScheme.primary, | ||||
|             iconEnabledColor: Theme.of(context).colorScheme.onBackground, | ||||
|             value: accountsBloc.activeAccount.value, | ||||
|             items: items, | ||||
|             onChanged: (final account) { | ||||
|               if (account == null) { | ||||
|                 return; | ||||
|               } | ||||
| 
 | ||||
|               accountsBloc.setActiveAccount(account); | ||||
|             }, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     return ResultBuilder<Capabilities>( | ||||
|       stream: capabilitiesBloc.capabilities, | ||||
|       builder: (final context, final capabilities) => DrawerHeader( | ||||
|         decoration: BoxDecoration( | ||||
|           color: Theme.of(context).colorScheme.primary, | ||||
|         ), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|           children: [ | ||||
|             if (capabilities.data != null) ...[ | ||||
|               if (capabilities.data!.capabilities.theming?.name != null) ...[ | ||||
|                 Text( | ||||
|                   capabilities.data!.capabilities.theming!.name!, | ||||
|                   style: DefaultTextStyle.of(context).style.copyWith( | ||||
|                         color: Theme.of(context).appBarTheme.foregroundColor, | ||||
|                       ), | ||||
|                 ), | ||||
|               ], | ||||
|               if (capabilities.data!.capabilities.theming?.logo != null) ...[ | ||||
|                 Flexible( | ||||
|                   child: NeonCachedUrlImage( | ||||
|                     url: capabilities.data!.capabilities.theming!.logo!, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ] else ...[ | ||||
|               NeonException( | ||||
|                 capabilities.error, | ||||
|                 onRetry: capabilitiesBloc.refresh, | ||||
|               ), | ||||
|               NeonLinearProgressIndicator( | ||||
|                 visible: capabilities.loading, | ||||
|               ), | ||||
|             ], | ||||
|             accountSelecor, | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,137 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:neon/neon.dart'; | ||||
| import 'package:rxdart/subjects.dart'; | ||||
| 
 | ||||
| typedef DestinationIconBuilder = Widget Function({Size size, Color color}); | ||||
| 
 | ||||
| class NeonNavigationDestination { | ||||
|   const NeonNavigationDestination({ | ||||
|     required this.label, | ||||
|     required this.icon, | ||||
|     this.selectedIcon, | ||||
|     this.notificationCount, | ||||
|   }); | ||||
| 
 | ||||
|   final String label; | ||||
|   final DestinationIconBuilder icon; | ||||
|   final Widget? selectedIcon; | ||||
|   final BehaviorSubject<int>? notificationCount; | ||||
| } | ||||
| 
 | ||||
| extension NavigationDestinationExtension on NavigationDestination { | ||||
|   static NavigationDestination fromNeonDestination(final NeonNavigationDestination neonDestination) => | ||||
|       NavigationDestination( | ||||
|         label: neonDestination.label, | ||||
|         icon: neonDestination.icon(), | ||||
|         selectedIcon: neonDestination.selectedIcon, | ||||
|       ); | ||||
| } | ||||
| 
 | ||||
| extension NavigationRailDestinationExtension on NavigationRailDestination { | ||||
|   static NavigationRailDestination fromNeonDestination(final NeonNavigationDestination neonDestination) { | ||||
|     final iconWIdget = StreamBuilder( | ||||
|       stream: neonDestination.notificationCount, | ||||
|       initialData: 0, | ||||
|       builder: (final context, final snapshot) { | ||||
|         final colorScheme = Theme.of(context).colorScheme; | ||||
| 
 | ||||
|         final color = snapshot.data! > 0 ? colorScheme.primary : colorScheme.onBackground; | ||||
|         const size = Size.square(kAvatarSize * 2 / 3); | ||||
| 
 | ||||
|         final icon = Container( | ||||
|           margin: const EdgeInsets.all(5), | ||||
|           child: neonDestination.icon(size: size, color: color), | ||||
|         ); | ||||
| 
 | ||||
|         if (snapshot.data! <= 0) { | ||||
|           return icon; | ||||
|         } | ||||
| 
 | ||||
|         final notificationIdicator = Builder( | ||||
|           builder: (final context) { | ||||
|             final style = TextStyle( | ||||
|               color: Theme.of(context).colorScheme.primary, | ||||
|               fontWeight: FontWeight.bold, | ||||
|             ); | ||||
| 
 | ||||
|             return Text( | ||||
|               snapshot.data!.toString(), | ||||
|               style: style, | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
| 
 | ||||
|         return Stack( | ||||
|           alignment: Alignment.bottomRight, | ||||
|           children: [ | ||||
|             icon, | ||||
|             notificationIdicator, | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     return NavigationRailDestination( | ||||
|       label: Text(neonDestination.label), | ||||
|       icon: iconWIdget, | ||||
|       selectedIcon: neonDestination.selectedIcon, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| extension NavigationDrawerDestinationExtension on NavigationDrawerDestination { | ||||
|   static NavigationDrawerDestination fromNeonDestination(final NeonNavigationDestination neonDestination) { | ||||
|     final labelWidget = StreamBuilder( | ||||
|       stream: neonDestination.notificationCount, | ||||
|       initialData: 0, | ||||
|       builder: (final context, final snapshot) { | ||||
|         final label = Text(neonDestination.label); | ||||
| 
 | ||||
|         if (snapshot.data! <= 0) { | ||||
|           return label; | ||||
|         } | ||||
| 
 | ||||
|         final notificationIdicator = Padding( | ||||
|           padding: const EdgeInsets.only(left: 12, right: 24), | ||||
|           child: Builder( | ||||
|             builder: (final context) { | ||||
|               final style = TextStyle( | ||||
|                 color: Theme.of(context).colorScheme.primary, | ||||
|                 fontWeight: FontWeight.bold, | ||||
|                 fontSize: 14, | ||||
|               ); | ||||
| 
 | ||||
|               return Text( | ||||
|                 snapshot.data!.toString(), | ||||
|                 style: style, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ); | ||||
| 
 | ||||
|         return Expanded( | ||||
|           child: Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|             children: [ | ||||
|               label, | ||||
|               notificationIdicator, | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     return NavigationDrawerDestination( | ||||
|       label: labelWidget, | ||||
|       icon: neonDestination.icon(), | ||||
|       selectedIcon: neonDestination.selectedIcon, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| extension TabExtension on Tab { | ||||
|   static Tab fromNeonDestination(final NeonNavigationDestination neonDestination) => Tab( | ||||
|         text: neonDestination.label, | ||||
|         icon: neonDestination.icon(), | ||||
|       ); | ||||
| } | ||||
					Loading…
					
					
				
		Reference in new issue