From 6472214a5769b9aafceb09d411d72448b087544c Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Wed, 14 Jun 2023 17:44:26 +0200 Subject: [PATCH 1/5] neon, neon_notifications: make AppImplementation.buildIcon a builder --- packages/neon/neon/lib/src/pages/home.dart | 2 +- packages/neon/neon/lib/src/pages/settings.dart | 2 +- .../neon/lib/src/utils/app_implementation.dart | 17 +++++++++-------- .../src/widgets/app_implementation_icon.dart | 1 - .../neon/neon_notifications/lib/pages/main.dart | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index b923a9f1..1e080866 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -356,7 +356,7 @@ class _HomePageState extends State { ], ], ), - leading: appImplementation.buildIcon(context), + leading: appImplementation.buildIcon(), minLeadingWidth: 0, onTap: () async { await _appsBloc.setActiveApp(appImplementation.id); diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index 2033cf5e..a0f5bfde 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -78,7 +78,7 @@ class _SettingsPageState extends State { for (final appImplementation in appImplementations) ...[ if (appImplementation.options.options.isNotEmpty) ...[ CustomSettingsTile( - leading: appImplementation.buildIcon(context), + leading: appImplementation.buildIcon(), title: Text(appImplementation.name(context)), onTap: () { NextcloudAppSettingsRoute(appid: appImplementation.id).go(context); diff --git a/packages/neon/neon/lib/src/utils/app_implementation.dart b/packages/neon/neon/lib/src/utils/app_implementation.dart index 63b789a5..961e5ca2 100644 --- a/packages/neon/neon/lib/src/utils/app_implementation.dart +++ b/packages/neon/neon/lib/src/utils/app_implementation.dart @@ -41,17 +41,18 @@ abstract class AppImplementation - SizedBox.fromSize( - size: size, - child: SvgPicture.asset( - 'assets/app.svg', - package: 'neon_$id', - colorFilter: ColorFilter.mode(color ?? Theme.of(context).colorScheme.primary, BlendMode.srcIn), + Builder( + builder: (final context) => SizedBox.fromSize( + size: size, + child: SvgPicture.asset( + 'assets/app.svg', + package: 'neon_$id', + colorFilter: ColorFilter.mode(color ?? Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), ), ); diff --git a/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart b/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart index 8434e778..c76c637a 100644 --- a/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart +++ b/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart @@ -24,7 +24,6 @@ class NeonAppImplementationIcon extends StatelessWidget { Container( margin: const EdgeInsets.all(5), child: appImplementation.buildIcon( - context, size: size, color: color, ), diff --git a/packages/neon/neon_notifications/lib/pages/main.dart b/packages/neon/neon_notifications/lib/pages/main.dart index c807037b..b1048fcd 100644 --- a/packages/neon/neon_notifications/lib/pages/main.dart +++ b/packages/neon/neon_notifications/lib/pages/main.dart @@ -74,7 +74,6 @@ class _NotificationsMainPageState extends State { ), leading: app != null ? app.buildIcon( - context, size: const Size.square(40), ) : SizedBox.fromSize( From ffbfd191e06a6e6f72af4b2f13d9a82a76532e7a Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 15 Jun 2023 07:11:57 +0200 Subject: [PATCH 2/5] neon: AppImplementation provide default getUnreadCounter neon: simplify AppImplementation.getUnreadCounter interface --- packages/neon/neon/lib/src/pages/home.dart | 3 ++- packages/neon/neon/lib/src/utils/app_implementation.dart | 2 +- packages/neon/neon_files/lib/neon_files.dart | 3 --- packages/neon/neon_news/lib/neon_news.dart | 2 +- packages/neon/neon_notes/lib/neon_notes.dart | 3 --- packages/neon/neon_notifications/lib/neon_notifications.dart | 3 +-- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 1e080866..b6984f94 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -461,7 +461,8 @@ class _HomePageState extends State { actions: [ if (notificationsAppImplementation.data != null) ...[ StreamBuilder( - stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), + stream: notificationsAppImplementation.data! + .getUnreadCounter(notificationsAppImplementation.data!.getBloc(account)), builder: (final context, final unreadCounterSnapshot) { final unreadCount = unreadCounterSnapshot.data ?? 0; return IconButton( diff --git a/packages/neon/neon/lib/src/utils/app_implementation.dart b/packages/neon/neon/lib/src/utils/app_implementation.dart index 961e5ca2..aa6ee3ee 100644 --- a/packages/neon/neon/lib/src/utils/app_implementation.dart +++ b/packages/neon/neon/lib/src/utils/app_implementation.dart @@ -37,7 +37,7 @@ abstract class AppImplementation? getUnreadCounter(final AppsBloc appsBloc); + BehaviorSubject? getUnreadCounter(final T bloc) => null; Widget get page; diff --git a/packages/neon/neon_files/lib/neon_files.dart b/packages/neon/neon_files/lib/neon_files.dart index 66a5c320..078b3504 100644 --- a/packages/neon/neon_files/lib/neon_files.dart +++ b/packages/neon/neon_files/lib/neon_files.dart @@ -63,7 +63,4 @@ class FilesApp extends AppImplementation { @override Widget get page => const FilesMainPage(); - - @override - BehaviorSubject? getUnreadCounter(final AppsBloc appsBloc) => null; } diff --git a/packages/neon/neon_news/lib/neon_news.dart b/packages/neon/neon_news/lib/neon_news.dart index 030a11fc..31fe7a7b 100644 --- a/packages/neon/neon_news/lib/neon_news.dart +++ b/packages/neon/neon_news/lib/neon_news.dart @@ -71,5 +71,5 @@ class NewsApp extends AppImplementation { Widget get page => const NewsMainPage(); @override - BehaviorSubject getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc(this).unreadCounter; + BehaviorSubject getUnreadCounter(final NewsBloc bloc) => bloc.unreadCounter; } diff --git a/packages/neon/neon_notes/lib/neon_notes.dart b/packages/neon/neon_notes/lib/neon_notes.dart index 2ca34cfb..65543b61 100644 --- a/packages/neon/neon_notes/lib/neon_notes.dart +++ b/packages/neon/neon_notes/lib/neon_notes.dart @@ -60,7 +60,4 @@ class NotesApp extends AppImplementation { @override Widget get page => const NotesMainPage(); - - @override - BehaviorSubject? getUnreadCounter(final AppsBloc appsBloc) => null; } diff --git a/packages/neon/neon_notifications/lib/neon_notifications.dart b/packages/neon/neon_notifications/lib/neon_notifications.dart index 83db56d7..d7fc163c 100644 --- a/packages/neon/neon_notifications/lib/neon_notifications.dart +++ b/packages/neon/neon_notifications/lib/neon_notifications.dart @@ -41,6 +41,5 @@ class NotificationsApp extends AppImplementation const NotificationsMainPage(); @override - BehaviorSubject getUnreadCounter(final AppsBloc appsBloc) => - appsBloc.getAppBloc(this).unreadCounter; + BehaviorSubject getUnreadCounter(final NotificationsBloc bloc) => bloc.unreadCounter; } From fee09e9ae05c6afc14c725c5146fa480f6de697c Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Wed, 14 Jun 2023 17:44:30 +0200 Subject: [PATCH 3/5] neon: deprecate NavigationMode.quickBar --- packages/neon/neon/lib/src/pages/home.dart | 415 +++++++----------- .../neon/lib/src/utils/global_options.dart | 2 + 2 files changed, 168 insertions(+), 249 deletions(-) diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index b6984f94..2b394b69 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -187,235 +187,159 @@ class _HomePageState extends State { return const Scaffold(); } - final isQuickBar = navigationMode == NavigationMode.quickBar; + final drawerAlwaysVisible = navigationMode == NavigationMode.drawerAlwaysVisible; + final drawer = Builder( builder: (final context) => Drawer( - width: isQuickBar ? kQuickBarWidth : null, - child: Container( - padding: isQuickBar ? const EdgeInsets.all(5) : null, - child: Column( - children: [ - Expanded( - child: Scrollbar( + child: Column( + children: [ + Expanded( + child: Scrollbar( + controller: drawerScrollController, + interactive: true, + child: ListView( controller: drawerScrollController, - interactive: true, - child: ListView( - controller: drawerScrollController, - // Needed for the drawer header to also render in the statusbar - padding: EdgeInsets.zero, - children: [ - Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - if (isQuickBar) { - return Column( - children: [ - if (accounts.length != 1) ...[ - for (final account in accounts) ...[ - Container( - margin: const EdgeInsets.symmetric( - vertical: 5, - ), - child: IconButton( - onPressed: () { - _accountsBloc.setActiveAccount(account); - }, - tooltip: account.client.humanReadableID, - icon: IntrinsicHeight( - child: NeonUserAvatar( - account: account, - ), + // Needed for the drawer header to also render in the statusbar + padding: EdgeInsets.zero, + children: [ + Builder( + builder: (final context) { + if (accountsSnapshot.hasData) { + return 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, ), - ), - ), - ], - Container( - margin: const EdgeInsets.only( - top: 10, - ), - child: Divider( - height: 5, - color: Theme.of(context).appBarTheme.foregroundColor, - ), ), ], - ], - ); - } - return 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!, ), - ], - 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, ), ], - if (accounts.length != 1) ...[ - DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - dropdownColor: Theme.of(context).colorScheme.primary, - iconEnabledColor: Theme.of(context).colorScheme.onBackground, - value: _account.id, - items: accounts - .map>( - (final account) => DropdownMenuItem( - value: account.id, - child: NeonAccountTile( - account: account, - dense: true, - textColor: - Theme.of(context).appBarTheme.foregroundColor, - ), + ] else ...[ + NeonException( + capabilities.error, + onRetry: _capabilitiesBloc.refresh, + ), + NeonLinearProgressIndicator( + visible: capabilities.loading, + ), + ], + if (accounts.length != 1) ...[ + DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + dropdownColor: Theme.of(context).colorScheme.primary, + iconEnabledColor: Theme.of(context).colorScheme.onBackground, + value: _account.id, + items: accounts + .map>( + (final account) => DropdownMenuItem( + value: account.id, + child: NeonAccountTile( + account: account, + dense: true, + textColor: + Theme.of(context).appBarTheme.foregroundColor, ), - ) - .toList(), - onChanged: (final id) { - if (id != null) { - _accountsBloc.setActiveAccount(accounts.find(id)!); - } - }, + ), + ) + .toList(), + onChanged: (final id) { + if (id != null) { + _accountsBloc.setActiveAccount(accounts.find(id)!); + } + }, + ), + ), + ], + ], + ), + ); + } + return Container(); + }, + ), + NeonException( + appImplementations.error, + onRetry: _appsBloc.refresh, + ), + NeonLinearProgressIndicator( + visible: appImplementations.loading, + ), + if (appImplementations.data != null) ...[ + for (final appImplementation in appImplementations.data!) ...[ + StreamBuilder( + stream: appImplementation.getUnreadCounter(_appsBloc), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + + return ListTile( + key: Key('app-${appImplementation.id}'), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(appImplementation.name(context)), + if (unreadCount > 0) ...[ + Text( + unreadCount.toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + fontSize: 14, ), ), ], ], ), - ); - } - return Container(); - }, - ), - NeonException( - appImplementations.error, - onlyIcon: isQuickBar, - onRetry: _appsBloc.refresh, - ), - NeonLinearProgressIndicator( - visible: appImplementations.loading, - ), - if (appImplementations.data != null) ...[ - for (final appImplementation in appImplementations.data!) ...[ - StreamBuilder( - stream: appImplementation.getUnreadCounter(_appsBloc) ?? - BehaviorSubject.seeded(0), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - if (isQuickBar) { - return IconButton( - onPressed: () async { - await _appsBloc.setActiveApp(appImplementation.id); - }, - tooltip: appImplementation.name(context), - icon: NeonAppImplementationIcon( - appImplementation: appImplementation, - unreadCount: unreadCount, - color: Theme.of(context).colorScheme.primary, - ), - ); - } - return ListTile( - key: Key('app-${appImplementation.id}'), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(appImplementation.name(context)), - if (unreadCount > 0) ...[ - Text( - unreadCount.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ], - ), - leading: appImplementation.buildIcon(), - minLeadingWidth: 0, - onTap: () async { - await _appsBloc.setActiveApp(appImplementation.id); + leading: appImplementation.buildIcon(), + minLeadingWidth: 0, + onTap: () async { + await _appsBloc.setActiveApp(appImplementation.id); - if (!mounted) { - return; - } - Scaffold.maybeOf(context)?.closeDrawer(); - }, - ); - }, - ), - ], + if (!mounted) { + return; + } + Scaffold.maybeOf(context)?.closeDrawer(); + }, + ); + }, + ), ], ], - ), + ], ), ), - if (isQuickBar) ...[ - IconButton( - onPressed: () => const SettingsRoute().go(context), - tooltip: AppLocalizations.of(context).settings, - icon: Icon( - Icons.settings, - color: Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ] else ...[ - ListTile( - key: const Key('settings'), - title: Text(AppLocalizations.of(context).settings), - leading: const Icon(Icons.settings), - minLeadingWidth: 0, - onTap: () async { - Scaffold.maybeOf(context)?.closeDrawer(); - const SettingsRoute().go(context); - }, - ), - ], - ], - ), + ), + ListTile( + key: const Key('settings'), + title: Text(AppLocalizations.of(context).settings), + leading: const Icon(Icons.settings), + minLeadingWidth: 0, + onTap: () async { + Scaffold.maybeOf(context)?.closeDrawer(); + const SettingsRoute().go(context); + }, + ), + ], ), ), ); final appBar = AppBar( - scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, - automaticallyImplyLeading: navigationMode == NavigationMode.drawer, - leadingWidth: isQuickBar ? kQuickBarWidth : null, - leading: isQuickBar - ? Container( - padding: const EdgeInsets.all(5), - child: capabilities.data?.capabilities.theming?.logo != null - ? NeonCachedUrlImage( - url: capabilities.data!.capabilities.theming!.logo!, - svgColor: Theme.of(context).iconTheme.color, - ) - : null, - ) - : null, + automaticallyImplyLeading: !drawerAlwaysVisible, title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -501,44 +425,52 @@ class _HomePageState extends State { ); Widget body = Builder( - builder: (final context) => Row( + builder: (final context) => Column( children: [ - if (navigationMode == NavigationMode.quickBar) ...[ - drawer, - ], - Expanded( - child: Column( - children: [ - if (appImplementations.data != null) ...[ - if (appImplementations.data!.isEmpty) ...[ - Expanded( - child: Center( - child: Text( - AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, - ), - ), - ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ - Expanded( - child: appImplementations.data!.find(activeAppIDSnapshot.data!)!.page, - ), - ], - ], - ], + if (appImplementations.data != null) ...[ + if (appImplementations.data!.isEmpty) ...[ + Expanded( + child: Center( + child: Text( + AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, + ), + ), + ), + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ + Expanded( + child: appImplementations.data!.find(activeAppIDSnapshot.data!)!.page, + ), ], - ), - ), + ], + ], ], ), ); body = MultiProvider( providers: _appsBloc.appBlocProviders, - child: body, + child: Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomInset: false, + drawer: !drawerAlwaysVisible ? drawer : null, + appBar: appBar, + body: body, + ), ); + if (drawerAlwaysVisible) { + body = Row( + children: [ + drawer, + Expanded( + child: body, + ), + ], + ); + } + return WillPopScope( onWillPop: () async { if (_scaffoldKey.currentState!.isDrawerOpen) { @@ -549,22 +481,7 @@ class _HomePageState extends State { _scaffoldKey.currentState!.openDrawer(); return false; }, - child: Row( - children: [ - if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ - drawer, - ], - Expanded( - child: Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - drawer: navigationMode == NavigationMode.drawer ? drawer : null, - appBar: appBar, - body: body, - ), - ), - ], - ), + child: body, ); }, ), diff --git a/packages/neon/neon/lib/src/utils/global_options.dart b/packages/neon/neon/lib/src/utils/global_options.dart index 462dfcce..8a8cb43b 100644 --- a/packages/neon/neon/lib/src/utils/global_options.dart +++ b/packages/neon/neon/lib/src/utils/global_options.dart @@ -224,6 +224,7 @@ class GlobalOptions { NavigationMode.drawerAlwaysVisible: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawerAlwaysVisible, }, + // ignore: deprecated_member_use_from_same_package NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar, }), ); @@ -232,5 +233,6 @@ class GlobalOptions { enum NavigationMode { drawer, drawerAlwaysVisible, + @Deprecated("The new design won't use this anymore") quickBar, } From b4ae60b1e5725830fda883c2b3f9f022badd052c Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Wed, 14 Jun 2023 17:44:30 +0200 Subject: [PATCH 4/5] neon: let apps provide a NavigationDestination --- .../lib/src/utils/app_implementation.dart | 12 ++ .../lib/src/widgets/drawer_destination.dart | 137 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 packages/neon/neon/lib/src/widgets/drawer_destination.dart diff --git a/packages/neon/neon/lib/src/utils/app_implementation.dart b/packages/neon/neon/lib/src/utils/app_implementation.dart index aa6ee3ee..9456afb3 100644 --- a/packages/neon/neon/lib/src/utils/app_implementation.dart +++ b/packages/neon/neon/lib/src/utils/app_implementation.dart @@ -41,6 +41,18 @@ abstract class AppImplementation(context, listen: false); + final account = accountsBloc.activeAccount.value!; + final bloc = getBloc(account); + + return NeonNavigationDestination( + label: name(context), + icon: buildIcon, + notificationCount: getUnreadCounter(bloc), + ); + } + Widget buildIcon({ final Size size = const Size.square(32), final Color? color, diff --git a/packages/neon/neon/lib/src/widgets/drawer_destination.dart b/packages/neon/neon/lib/src/widgets/drawer_destination.dart new file mode 100644 index 00000000..8d0824e2 --- /dev/null +++ b/packages/neon/neon/lib/src/widgets/drawer_destination.dart @@ -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? 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(), + ); +} From 8cdee9cb00aa2062091170343d0edb97804c2d11 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Wed, 14 Jun 2023 17:44:30 +0200 Subject: [PATCH 5/5] neon: refactor Drawer make neon use a NavigationDrawer migrating the drawer to m3 --- packages/neon/neon/lib/neon.dart | 2 + packages/neon/neon/lib/src/pages/home.dart | 150 +------------ .../neon/neon/lib/src/widgets/drawer.dart | 210 ++++++++++++++++++ packages/neon/neon/pubspec.yaml | 1 + 4 files changed, 214 insertions(+), 149 deletions(-) create mode 100644 packages/neon/neon/lib/src/widgets/drawer.dart diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index b84514e6..a2093959 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -25,6 +25,8 @@ import 'package:neon/src/blocs/blocs.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/push_notification.dart'; import 'package:neon/src/router.dart'; +import 'package:neon/src/widgets/drawer.dart'; +import 'package:neon/src/widgets/drawer_destination.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart' as p; diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 2b394b69..9eef37df 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -189,155 +189,7 @@ class _HomePageState extends State { final drawerAlwaysVisible = navigationMode == NavigationMode.drawerAlwaysVisible; - final drawer = Builder( - builder: (final context) => Drawer( - child: Column( - children: [ - Expanded( - child: Scrollbar( - controller: drawerScrollController, - interactive: true, - child: ListView( - controller: drawerScrollController, - // Needed for the drawer header to also render in the statusbar - padding: EdgeInsets.zero, - children: [ - Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - return 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, - ), - ], - if (accounts.length != 1) ...[ - DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - dropdownColor: Theme.of(context).colorScheme.primary, - iconEnabledColor: Theme.of(context).colorScheme.onBackground, - value: _account.id, - items: accounts - .map>( - (final account) => DropdownMenuItem( - value: account.id, - child: NeonAccountTile( - account: account, - dense: true, - textColor: - Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ) - .toList(), - onChanged: (final id) { - if (id != null) { - _accountsBloc.setActiveAccount(accounts.find(id)!); - } - }, - ), - ), - ], - ], - ), - ); - } - return Container(); - }, - ), - NeonException( - appImplementations.error, - onRetry: _appsBloc.refresh, - ), - NeonLinearProgressIndicator( - visible: appImplementations.loading, - ), - if (appImplementations.data != null) ...[ - for (final appImplementation in appImplementations.data!) ...[ - StreamBuilder( - stream: appImplementation.getUnreadCounter(_appsBloc), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - - return ListTile( - key: Key('app-${appImplementation.id}'), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(appImplementation.name(context)), - if (unreadCount > 0) ...[ - Text( - unreadCount.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ], - ), - leading: appImplementation.buildIcon(), - minLeadingWidth: 0, - onTap: () async { - await _appsBloc.setActiveApp(appImplementation.id); - - if (!mounted) { - return; - } - Scaffold.maybeOf(context)?.closeDrawer(); - }, - ); - }, - ), - ], - ], - ], - ), - ), - ), - ListTile( - key: const Key('settings'), - title: Text(AppLocalizations.of(context).settings), - leading: const Icon(Icons.settings), - minLeadingWidth: 0, - onTap: () async { - Scaffold.maybeOf(context)?.closeDrawer(); - const SettingsRoute().go(context); - }, - ), - ], - ), - ), - ); + const drawer = NeonDrawer(); final appBar = AppBar( automaticallyImplyLeading: !drawerAlwaysVisible, title: Column( diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart new file mode 100644 index 00000000..8cb8cec0 --- /dev/null +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -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(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 apps; + + @override + State<_NeonDrawer> createState() => __NeonDrawerState(); +} + +class __NeonDrawerState extends State<_NeonDrawer> with SingleTickerProviderStateMixin { + late TabController _tabController; + late AccountsBloc _accountsBloc; + late AppsBloc _appsBloc; + late List _apps; + + int _activeApp = 0; + + @override + void initState() { + super.initState(); + + _accountsBloc = Provider.of(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(context, listen: false); + final capabilitiesBloc = accountsBloc.activeCapabilitiesBloc; + + final accountSelecor = StreamBuilder>( + 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( + 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, + ], + ), + ), + ); + } +} diff --git a/packages/neon/neon/pubspec.yaml b/packages/neon/neon/pubspec.yaml index f0d6865c..8d0b84aa 100644 --- a/packages/neon/neon/pubspec.yaml +++ b/packages/neon/neon/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: intl: ^0.18.0 json_annotation: ^4.8.1 material_design_icons_flutter: ^7.0.7296 + meta: ^1.9.1 nextcloud: git: url: https://github.com/provokateurin/nextcloud-neon