Nikolas Rimikis
1 year ago
committed by
GitHub
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