diff --git a/packages/neon/lib/src/blocs/apps.dart b/packages/neon/lib/src/blocs/apps.dart index c793172a..da6b957e 100644 --- a/packages/neon/lib/src/blocs/apps.dart +++ b/packages/neon/lib/src/blocs/apps.dart @@ -14,6 +14,8 @@ abstract class AppsBlocStates { BehaviorSubject> get notificationsAppImplementation; BehaviorSubject get activeAppID; + + BehaviorSubject get openNotifications; } class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates { @@ -88,6 +90,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates unawaited(appImplementations.close()); unawaited(notificationsAppImplementation.close()); unawaited(activeAppID.close()); + unawaited(openNotifications.close()); for (final key in _blocs.keys) { _blocs[key]!.dispose(); } @@ -107,6 +110,9 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates BehaviorSubject> notificationsAppImplementation = BehaviorSubject>(); + @override + BehaviorSubject openNotifications = BehaviorSubject(); + @override Future refresh() async { await _requestManager.wrapNextcloud, NextcloudCoreNavigationApps>( @@ -128,7 +134,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates activeAppID.add(appID); } } else if (appID == 'notifications') { - // TODO: Open notifications page + openNotifications.add(null); } else { throw Exception('App $appID not found'); } diff --git a/packages/neon/lib/src/pages/home.dart b/packages/neon/lib/src/pages/home.dart index 580a3b62..bc0e14f9 100644 --- a/packages/neon/lib/src/pages/home.dart +++ b/packages/neon/lib/src/pages/home.dart @@ -21,8 +21,9 @@ class _HomePageState extends State { final _scaffoldKey = GlobalKey(); late GlobalOptions _globalOptions; - late CapabilitiesBloc _capabilitiesBloc; + late AccountsBloc _accountsBloc; late AppsBloc _appsBloc; + late CapabilitiesBloc _capabilitiesBloc; late FirstLaunchBloc _firstLaunchBloc; @override @@ -30,10 +31,22 @@ class _HomePageState extends State { super.initState(); _globalOptions = Provider.of(context, listen: false); - _appsBloc = Provider.of(context, listen: false).getAppsBloc(widget.account); - _capabilitiesBloc = Provider.of(context, listen: false).getCapabilitiesBloc(widget.account); + _accountsBloc = Provider.of(context, listen: false); + _appsBloc = _accountsBloc.getAppsBloc(widget.account); + _capabilitiesBloc = _accountsBloc.getCapabilitiesBloc(widget.account); _firstLaunchBloc = Provider.of(context, listen: false); + _appsBloc.openNotifications.listen((final _) async { + final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; + if (notificationsAppImplementation != null) { + await _openNotifications( + notificationsAppImplementation.data!, + _accountsBloc.accounts.value, + _accountsBloc.activeAccount.value!, + ); + } + }); + _capabilitiesBloc.capabilities.listen((final result) async { if (result.data != null) { widget.onThemeChanged(result.data!.capabilities.theming); @@ -153,6 +166,34 @@ class _HomePageState extends State { ); } + Future _openNotifications( + final NotificationsApp app, + final List accounts, + final Account account, + ) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(app.name(context)), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ], + ), + ), + body: app.buildPage(context, _appsBloc), + ), + ), + ); + } + @override void dispose() { _capabilitiesBloc.dispose(); @@ -160,450 +201,432 @@ class _HomePageState extends State { } @override - Widget build(final BuildContext context) { - final accountsBloc = Provider.of(context, listen: false); - return ResultBuilder( - stream: _capabilitiesBloc.capabilities, - builder: (final context, final capabilities) => ResultBuilder>( - stream: _appsBloc.appImplementations, - builder: (final context, final appImplementations) => ResultBuilder( - stream: _appsBloc.notificationsAppImplementation, - builder: (final context, final notificationsAppImplementation) => StreamBuilder( - stream: _appsBloc.activeAppID, - builder: ( - final context, - final activeAppIDSnapshot, - ) => - StreamBuilder>( - stream: accountsBloc.accounts, + Widget build(final BuildContext context) => ResultBuilder( + stream: _capabilitiesBloc.capabilities, + builder: (final context, final capabilities) => ResultBuilder>( + stream: _appsBloc.appImplementations, + builder: (final context, final appImplementations) => ResultBuilder( + stream: _appsBloc.notificationsAppImplementation, + builder: (final context, final notificationsAppImplementation) => StreamBuilder( + stream: _appsBloc.activeAppID, builder: ( final context, - final accountsSnapshot, + final activeAppIDSnapshot, ) => - OptionBuilder( - option: _globalOptions.navigationMode, - builder: (final context, final navigationMode) => WillPopScope( - onWillPop: () async { - if (_scaffoldKey.currentState!.isDrawerOpen) { - Navigator.pop(context); - return true; - } + StreamBuilder>( + stream: _accountsBloc.accounts, + builder: ( + final context, + final accountsSnapshot, + ) => + OptionBuilder( + option: _globalOptions.navigationMode, + builder: (final context, final navigationMode) => WillPopScope( + onWillPop: () async { + if (_scaffoldKey.currentState!.isDrawerOpen) { + Navigator.pop(context); + return true; + } - _scaffoldKey.currentState!.openDrawer(); - return false; - }, - child: Builder( - builder: (final context) { - if (accountsSnapshot.hasData) { - final accounts = accountsSnapshot.data!; - final account = accounts.singleWhere((final account) => account.id == widget.account.id); + _scaffoldKey.currentState!.openDrawer(); + return false; + }, + child: Builder( + builder: (final context) { + if (accountsSnapshot.hasData) { + final accounts = accountsSnapshot.data!; + final account = accounts.singleWhere((final account) => account.id == widget.account.id); - final isQuickBar = navigationMode == NavigationMode.quickBar; - final drawer = Drawer( - width: isQuickBar ? kQuickBarWidth : null, - child: Container( - padding: isQuickBar ? const EdgeInsets.all(5) : null, - child: Column( - children: [ - Expanded( - child: Scrollbar( - child: ListView( - // 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: Tooltip( - message: account.client.humanReadableID, - child: IconButton( - onPressed: () { - accountsBloc.setActiveAccount(account); - }, - icon: IntrinsicHeight( - child: AccountAvatar( - account: account, + final isQuickBar = navigationMode == NavigationMode.quickBar; + final drawer = Drawer( + width: isQuickBar ? kQuickBarWidth : null, + child: Container( + padding: isQuickBar ? const EdgeInsets.all(5) : null, + child: Column( + children: [ + Expanded( + child: Scrollbar( + child: ListView( + // 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: Tooltip( + message: account.client.humanReadableID, + child: IconButton( + onPressed: () { + _accountsBloc.setActiveAccount(account); + }, + icon: IntrinsicHeight( + child: AccountAvatar( + account: account, + ), ), ), ), ), - ), - ], - 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: CachedURLImage( - url: capabilities.data!.capabilities.theming!.logo!, + ], + Container( + margin: const EdgeInsets.only( + top: 10, + ), + child: Divider( + height: 5, + color: Theme.of(context).appBarTheme.foregroundColor, ), ), ], - ] else ...[ - ExceptionWidget( - capabilities.error, - onRetry: _capabilitiesBloc.refresh, - ), - CustomLinearProgressIndicator( - 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: widget.account.id, - items: accounts - .map>( - (final account) => DropdownMenuItem( - value: account.id, - child: AccountTile( - account: account, - dense: true, - textColor: - Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ) - .toList(), - onChanged: (final id) { - for (final account in accounts) { - if (account.id == id) { - accountsBloc.setActiveAccount(account); - break; - } - } - }, - ), - ), - ], - ], - ), - ); - } - return Container(); - }, - ), - ExceptionWidget( - appImplementations.error, - onlyIcon: isQuickBar, - onRetry: _appsBloc.refresh, - ), - CustomLinearProgressIndicator( - 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 Tooltip( - message: appImplementation.name(context), - child: IconButton( - onPressed: () async { - await _appsBloc.setActiveApp(appImplementation.id); - }, - icon: AppImplementationIcon( - appImplementation: appImplementation, - unreadCount: unreadCount, - color: Theme.of(context).colorScheme.primary, - ), - ), ); } - return ListTile( - key: Key('app-${appImplementation.id}'), - title: Row( + return DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, 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, + 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: CachedURLImage( + url: capabilities.data!.capabilities.theming!.logo!, + ), + ), + ], + ] else ...[ + ExceptionWidget( + capabilities.error, + onRetry: _capabilitiesBloc.refresh, + ), + CustomLinearProgressIndicator( + 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: widget.account.id, + items: accounts + .map>( + (final account) => DropdownMenuItem( + value: account.id, + child: AccountTile( + account: account, + dense: true, + textColor: + Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ) + .toList(), + onChanged: (final id) { + for (final account in accounts) { + if (account.id == id) { + _accountsBloc.setActiveAccount(account); + break; + } + } + }, ), ), ], ], ), - leading: appImplementation.buildIcon(context), - minLeadingWidth: 0, - onTap: () async { - await _appsBloc.setActiveApp(appImplementation.id); - if (navigationMode == NavigationMode.drawer) { - // Don't pop when the drawer is always shown - if (!mounted) { - return; - } - Navigator.of(context).pop(); - } - }, ); - }, - ), + } + return Container(); + }, + ), + ExceptionWidget( + appImplementations.error, + onlyIcon: isQuickBar, + onRetry: _appsBloc.refresh, + ), + CustomLinearProgressIndicator( + 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 Tooltip( + message: appImplementation.name(context), + child: IconButton( + onPressed: () async { + await _appsBloc.setActiveApp(appImplementation.id); + }, + icon: AppImplementationIcon( + 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(context), + minLeadingWidth: 0, + onTap: () async { + await _appsBloc.setActiveApp(appImplementation.id); + if (navigationMode == NavigationMode.drawer) { + // Don't pop when the drawer is always shown + if (!mounted) { + return; + } + Navigator.of(context).pop(); + } + }, + ); + }, + ), + ], ], ], - ], + ), ), ), - ), - if (isQuickBar) ...[ - IconButton( - icon: Icon( - Icons.settings, - color: Theme.of(context).appBarTheme.foregroundColor, + if (isQuickBar) ...[ + IconButton( + icon: Icon( + Icons.settings, + color: Theme.of(context).appBarTheme.foregroundColor, + ), + onPressed: _openSettings, ), - onPressed: _openSettings, - ), - ] else ...[ - ListTile( - key: const Key('settings'), - title: Text(AppLocalizations.of(context).settings), - leading: const Icon(Icons.settings), - minLeadingWidth: 0, - onTap: () async { - if (navigationMode == NavigationMode.drawer) { - Navigator.of(context).pop(); - } - await _openSettings(); - }, - ), + ] else ...[ + ListTile( + key: const Key('settings'), + title: Text(AppLocalizations.of(context).settings), + leading: const Icon(Icons.settings), + minLeadingWidth: 0, + onTap: () async { + if (navigationMode == NavigationMode.drawer) { + Navigator.of(context).pop(); + } + await _openSettings(); + }, + ), + ], ], - ], + ), ), - ), - ); + ); - return Scaffold( - resizeToAvoidBottomInset: false, - body: Row( - children: [ - if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ - drawer, - ], - Expanded( - child: Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - drawer: navigationMode == NavigationMode.drawer ? drawer : null, - 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 - ? CachedURLImage( - url: capabilities.data!.capabilities.theming!.logo!, - ) - : null, - ) - : null, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ - Flexible( - child: Text( - appImplementations.data! - .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) - .name(context), + return Scaffold( + resizeToAvoidBottomInset: false, + body: Row( + children: [ + if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ + drawer, + ], + Expanded( + child: Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomInset: false, + drawer: navigationMode == NavigationMode.drawer ? drawer : null, + 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 + ? CachedURLImage( + url: capabilities.data!.capabilities.theming!.logo!, + ) + : null, + ) + : null, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ + Flexible( + child: Text( + appImplementations.data! + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .name(context), + ), ), - ), - ], - if (appImplementations.error != null) ...[ - const SizedBox( - width: 8, - ), - Icon( - Icons.error_outline, - size: 30, - color: Theme.of(context).colorScheme.onPrimary, - ), - ], - if (appImplementations.loading) ...[ - const SizedBox( - width: 8, - ), - Expanded( - child: CustomLinearProgressIndicator( - color: Theme.of(context).appBarTheme.foregroundColor, + ], + if (appImplementations.error != null) ...[ + const SizedBox( + width: 8, ), - ), + Icon( + Icons.error_outline, + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), + ], + if (appImplementations.loading) ...[ + const SizedBox( + width: 8, + ), + Expanded( + child: CustomLinearProgressIndicator( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ], ], + ), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall, + ), ], - ), - if (accounts.length > 1) ...[ - Text( - account.client.humanReadableID, - style: Theme.of(context).textTheme.bodySmall, + ], + ), + actions: [ + if (notificationsAppImplementation.data != null) ...[ + StreamBuilder( + stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + return IconButton( + key: Key('app-${notificationsAppImplementation.data!.id}'), + icon: AppImplementationIcon( + appImplementation: notificationsAppImplementation.data!, + unreadCount: unreadCount, + color: unreadCount > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onBackground, + width: kAvatarSize * 2 / 3, + height: kAvatarSize * 2 / 3, + ), + onPressed: () async { + await _openNotifications( + notificationsAppImplementation.data!, + accounts, + account, + ); + }, + ); + }, ), ], - ], - ), - actions: [ - if (notificationsAppImplementation.data != null) ...[ - StreamBuilder( - stream: notificationsAppImplementation.data!.getUnreadCounter(_appsBloc), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - return IconButton( - key: Key('app-${notificationsAppImplementation.data!.id}'), - icon: AppImplementationIcon( - appImplementation: notificationsAppImplementation.data!, - unreadCount: unreadCount, - color: unreadCount > 0 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, - width: kAvatarSize * 2 / 3, - height: kAvatarSize * 2 / 3, + IconButton( + icon: IntrinsicWidth( + child: AccountAvatar( + account: account, + ), + ), + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => AccountSettingsPage( + bloc: _accountsBloc, + account: account, + ), ), - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => Scaffold( - appBar: AppBar( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(notificationsAppImplementation.data!.name(context)), - if (accounts.length > 1) ...[ - Text( - account.client.humanReadableID, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ], - ), - ), - body: notificationsAppImplementation.data! - .buildPage(context, _appsBloc), - ), - ), - ); - }, ); }, ), ], - IconButton( - icon: IntrinsicWidth( - child: AccountAvatar( - account: account, - ), - ), - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => AccountSettingsPage( - bloc: accountsBloc, - account: account, + ), + body: Row( + children: [ + if (navigationMode == NavigationMode.quickBar) ...[ + drawer, + ], + Expanded( + child: Column( + children: [ + ExceptionWidget( + appImplementations.error, + onRetry: _appsBloc.refresh, ), - ), - ); - }, - ), - ], - ), - body: Row( - children: [ - if (navigationMode == NavigationMode.quickBar) ...[ - drawer, - ], - Expanded( - child: Column( - children: [ - ExceptionWidget( - appImplementations.error, - onRetry: _appsBloc.refresh, - ), - if (appImplementations.data != null) ...[ - if (appImplementations.data!.isEmpty) ...[ - Expanded( - child: Center( - child: Text( - AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, - ), - ), - ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ + if (appImplementations.data != null) ...[ + if (appImplementations.data!.isEmpty) ...[ Expanded( - child: appImplementations.data! - .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) - .buildPage(context, _appsBloc), + child: Center( + child: Text( + AppLocalizations.of(context) + .errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, + ), + ), ), + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ + Expanded( + child: appImplementations.data! + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .buildPage(context, _appsBloc), + ), + ], ], ], ], - ], + ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - } - return Container(); - }, + ], + ), + ); + } + return Container(); + }, + ), ), ), ), ), ), ), - ), - ); - } + ); }