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