diff --git a/packages/neon/integration_test/screenshot_test.dart b/packages/neon/integration_test/screenshot_test.dart index 2225b0cd..483092c6 100644 --- a/packages/neon/integration_test/screenshot_test.dart +++ b/packages/neon/integration_test/screenshot_test.dart @@ -486,7 +486,8 @@ Future main() async { ), ); await prepareScreenshot(tester, binding); - await switchPage(tester, 'app-notifications'); + await tester.tap(find.byKey(const Key('app-notifications'))); + await tester.pumpAndSettle(); await tester.pumpAndSettle(); await tester.pump(); diff --git a/packages/neon/lib/src/apps/news/app.dart b/packages/neon/lib/src/apps/news/app.dart index f5ff6da6..f822727c 100644 --- a/packages/neon/lib/src/apps/news/app.dart +++ b/packages/neon/lib/src/apps/news/app.dart @@ -71,5 +71,5 @@ class NewsApp extends AppImplementation { ); @override - BehaviorSubject? getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc(this).unreadCounter; + BehaviorSubject getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc(this).unreadCounter; } diff --git a/packages/neon/lib/src/apps/notifications/app.dart b/packages/neon/lib/src/apps/notifications/app.dart index 8e2197cd..2a3cb8cb 100644 --- a/packages/neon/lib/src/apps/notifications/app.dart +++ b/packages/neon/lib/src/apps/notifications/app.dart @@ -40,6 +40,6 @@ class NotificationsApp extends AppImplementation? getUnreadCounter(final AppsBloc appsBloc) => + BehaviorSubject getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc(this).unreadCounter; } diff --git a/packages/neon/lib/src/blocs/accounts.dart b/packages/neon/lib/src/blocs/accounts.dart index 6fc7e150..a2b8d3f1 100644 --- a/packages/neon/lib/src/blocs/accounts.dart +++ b/packages/neon/lib/src/blocs/accounts.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:neon/src/blocs/apps.dart'; +import 'package:neon/src/blocs/capabilities.dart'; import 'package:neon/src/blocs/user_details.dart'; import 'package:neon/src/blocs/user_status.dart'; import 'package:neon/src/models/account.dart'; @@ -127,18 +128,30 @@ class AccountsBloc extends $AccountsBloc { ); AppsBloc getAppsBloc(final Account account) { - if (_accountsAppsBlocs[account.id] != null) { - return _accountsAppsBlocs[account.id]!; + if (_appsBlocs[account.id] != null) { + return _appsBlocs[account.id]!; } - return _accountsAppsBlocs[account.id] = AppsBloc( + return _appsBlocs[account.id] = AppsBloc( _requestManager, + getCapabilitiesBloc(account), this, account, _allAppImplementations, ); } + CapabilitiesBloc getCapabilitiesBloc(final Account account) { + if (_capabilitiesBlocs[account.id] != null) { + return _capabilitiesBlocs[account.id]!; + } + + return _capabilitiesBlocs[account.id] = CapabilitiesBloc( + _requestManager, + account.client, + ); + } + UserDetailsBloc getUserDetailsBloc(final Account account) { if (_userDetailsBlocs[account.id] != null) { return _userDetailsBlocs[account.id]!; @@ -176,7 +189,8 @@ class AccountsBloc extends $AccountsBloc { late final _activeAccountSubject = BehaviorSubject.seeded(null); late final _accountsSubject = BehaviorSubject>.seeded([]); - final _accountsAppsBlocs = {}; + final _appsBlocs = {}; + final _capabilitiesBlocs = {}; final _userDetailsBlocs = {}; final _userStatusBlocs = {}; diff --git a/packages/neon/lib/src/blocs/apps.dart b/packages/neon/lib/src/blocs/apps.dart index 1097cf09..90732b4a 100644 --- a/packages/neon/lib/src/blocs/apps.dart +++ b/packages/neon/lib/src/blocs/apps.dart @@ -1,7 +1,8 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'package:neon/src/apps/notifications/app.dart'; import 'package:neon/src/blocs/accounts.dart'; +import 'package:neon/src/blocs/capabilities.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/neon.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -22,6 +23,8 @@ abstract class AppsBlocStates { BehaviorSubject>> get appImplementations; + BehaviorSubject> get notificationsAppImplementation; + BehaviorSubject get activeAppID; } @@ -29,6 +32,7 @@ abstract class AppsBlocStates { class AppsBloc extends $AppsBloc { AppsBloc( this._requestManager, + this._capabilitiesBloc, this._accountsBloc, this._account, this._allAppImplementations, @@ -40,28 +44,31 @@ class AppsBloc extends $AppsBloc { if (_activeAppSubject.valueOrNull != appId) { _activeAppSubject.add(appId); } + } else if (appId == 'notifications') { + // TODO: Open notifications page } else { - debugPrint('App $appId not found'); + throw Exception('App $appId not found'); } }); _appsSubject.listen((final result) { if (result is ResultLoading) { _appImplementationsSubject.add(Result.loading()); - } else if (result is ResultError) { - _appImplementationsSubject.add(Result.error((result as ResultError).error)); - } else if (result is ResultSuccess) { + } else if (result is ResultError>) { + _appImplementationsSubject.add(Result.error(result.error)); + } else if (result is ResultSuccess>) { _appImplementationsSubject.add( - Result.success(_filteredAppImplementations((result as ResultSuccess>).data)), + Result.success(_filteredAppImplementations(result.data.map((final a) => a.id).toList())), ); - } else if (result is ResultCached && result.data != null) { + } else if (result is ResultCached>) { _appImplementationsSubject.add( - ResultCached(_filteredAppImplementations((result as ResultCached>).data)), + ResultCached(_filteredAppImplementations(result.data.map((final a) => a.id).toList())), ); } - final appImplementations = - result.data != null ? _filteredAppImplementations(result.data!) : []; + final appImplementations = result.data != null + ? _filteredAppImplementations(result.data!.map((final a) => a.id).toList()) + : []; if (result.data != null) { final options = _accountsBloc.getOptions(_account); @@ -84,16 +91,39 @@ class AppsBloc extends $AppsBloc { } }); + _capabilitiesBloc.capabilities.listen((final result) { + if (result is ResultLoading) { + _notificationsAppImplementationSubject.add(Result.loading()); + } else if (result is ResultError) { + _notificationsAppImplementationSubject.add(Result.error(result.error)); + } else if (result is ResultSuccess) { + _notificationsAppImplementationSubject.add( + Result.success( + result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null, + ), + ); + } else if (result is ResultCached) { + _notificationsAppImplementationSubject.add( + ResultCached(result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null), + ); + } + }); + _loadApps(); } - final _extraApps = ['notifications']; + T? _findAppImplementation(final String id) { + final matches = _filteredAppImplementations([id]); + if (matches.isNotEmpty) { + return matches.single as T; + } - List _filteredAppImplementations(final List apps) { - final appIds = apps.map((final a) => a.id).toList(); - return _allAppImplementations.where((final a) => appIds.contains(a.id) || _extraApps.contains(a.id)).toList(); + return null; } + List _filteredAppImplementations(final List appIds) => + _allAppImplementations.where((final a) => appIds.contains(a.id)).toList(); + void _loadApps() { _requestManager .wrapNextcloud, CoreNavigationApps>( @@ -107,12 +137,14 @@ class AppsBloc extends $AppsBloc { } final RequestManager _requestManager; + final CapabilitiesBloc _capabilitiesBloc; final AccountsBloc _accountsBloc; final Account _account; final List _allAppImplementations; final _appsSubject = BehaviorSubject>>(); final _appImplementationsSubject = BehaviorSubject>>(); + final _notificationsAppImplementationSubject = BehaviorSubject>(); late final _activeAppSubject = BehaviorSubject(); final Map _blocs = {}; @@ -128,6 +160,8 @@ class AppsBloc extends $AppsBloc { @override void dispose() { unawaited(_appsSubject.close()); + unawaited(_appImplementationsSubject.close()); + unawaited(_notificationsAppImplementationSubject.close()); unawaited(_activeAppSubject.close()); for (final key in _blocs.keys) { _blocs[key]!.dispose(); @@ -142,6 +176,10 @@ class AppsBloc extends $AppsBloc { BehaviorSubject>>> _mapToAppImplementationsState() => _appImplementationsSubject; + @override + BehaviorSubject> _mapToNotificationsAppImplementationState() => + _notificationsAppImplementationSubject; + @override BehaviorSubject _mapToActiveAppIDState() => _activeAppSubject; } diff --git a/packages/neon/lib/src/blocs/apps.rxb.g.dart b/packages/neon/lib/src/blocs/apps.rxb.g.dart index d06734a0..85ea9af4 100644 --- a/packages/neon/lib/src/blocs/apps.rxb.g.dart +++ b/packages/neon/lib/src/blocs/apps.rxb.g.dart @@ -32,6 +32,11 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS late final BehaviorSubject>>> _appImplementationsState = _mapToAppImplementationsState(); + /// The state of [notificationsAppImplementation] implemented in + /// [_mapToNotificationsAppImplementationState] + late final BehaviorSubject> _notificationsAppImplementationState = + _mapToNotificationsAppImplementationState(); + /// The state of [activeAppID] implemented in [_mapToActiveAppIDState] late final BehaviorSubject _activeAppIDState = _mapToActiveAppIDState(); @@ -48,6 +53,9 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS BehaviorSubject>>> get appImplementations => _appImplementationsState; + @override + BehaviorSubject> get notificationsAppImplementation => _notificationsAppImplementationState; + @override BehaviorSubject get activeAppID => _activeAppIDState; @@ -56,6 +64,8 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS BehaviorSubject>>> _mapToAppImplementationsState(); + BehaviorSubject> _mapToNotificationsAppImplementationState(); + BehaviorSubject _mapToActiveAppIDState(); @override diff --git a/packages/neon/lib/src/neon.dart b/packages/neon/lib/src/neon.dart index f6c3e5e8..4de0969a 100644 --- a/packages/neon/lib/src/neon.dart +++ b/packages/neon/lib/src/neon.dart @@ -56,9 +56,9 @@ import 'package:window_manager/window_manager.dart'; import 'package:xdg_directories/xdg_directories.dart' as xdg; part 'app.dart'; +part 'pages/account_settings.dart'; part 'pages/home.dart'; part 'pages/login.dart'; -part 'pages/account_settings.dart'; part 'pages/nextcloud_app_settings.dart'; part 'pages/settings.dart'; part 'platform/abstract.dart'; @@ -88,6 +88,7 @@ part 'utils/validators.dart'; part 'widgets/account_avatar.dart'; part 'widgets/account_settings_tile.dart'; part 'widgets/account_tile.dart'; +part 'widgets/app_implementation_icon.dart'; part 'widgets/cached_api_image.dart'; part 'widgets/cached_image.dart'; part 'widgets/cached_url_image.dart'; diff --git a/packages/neon/lib/src/pages/home.dart b/packages/neon/lib/src/pages/home.dart index 6e1fb595..cc4d716c 100644 --- a/packages/neon/lib/src/pages/home.dart +++ b/packages/neon/lib/src/pages/home.dart @@ -30,11 +30,8 @@ class _HomePageState extends State { _globalOptions = Provider.of(context, listen: false); _appsBloc = RxBlocProvider.of(context).getAppsBloc(widget.account); + _capabilitiesBloc = RxBlocProvider.of(context).getCapabilitiesBloc(widget.account); - _capabilitiesBloc = CapabilitiesBloc( - Provider.of(context, listen: false), - widget.account.client, - ); _capabilitiesBloc.capabilities.listen((final result) async { if (result.data != null) { widget.onThemeChanged(result.data!.capabilities.theming!); @@ -162,411 +159,448 @@ class _HomePageState extends State { final appsLoading, final _, ) => - RxBlocBuilder( + StandardRxResultBuilder( bloc: _appsBloc, - state: (final bloc) => bloc.activeAppID, + state: (final bloc) => bloc.notificationsAppImplementation, builder: ( final context, - final activeAppIDSnapshot, + final notificationsAppData, + final notificationsAppError, + final notificationsAppLoading, final _, ) => - RxBlocBuilder>( - bloc: accountsBloc, - state: (final bloc) => bloc.accounts, + RxBlocBuilder( + bloc: _appsBloc, + state: (final bloc) => bloc.activeAppID, builder: ( final context, - final accountsSnapshot, + final activeAppIDSnapshot, final _, ) => - OptionBuilder( - option: _globalOptions.navigationMode, - builder: (final context, final navigationMode) => WillPopScope( - onWillPop: () async { - if (_scaffoldKey.currentState!.isDrawerOpen) { - Navigator.pop(context); - return true; - } + RxBlocBuilder>( + bloc: accountsBloc, + state: (final bloc) => bloc.accounts, + builder: ( + final context, + final accountsSnapshot, + final _, + ) => + 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, + ), ), ], - Container( - margin: const EdgeInsets.only( - top: 10, + ], + ); + } + return DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (capabilitiesData != null) ...[ + Text( + capabilitiesData.capabilities.theming!.name, + style: DefaultTextStyle.of(context).style.copyWith( + color: Theme.of(context).appBarTheme.foregroundColor, + ), ), - child: Divider( - height: 5, - color: Theme.of(context).appBarTheme.foregroundColor, + Flexible( + child: CachedURLImage( + url: capabilitiesData.capabilities.theming!.logo, + ), ), - ), + ] else ...[ + ExceptionWidget( + capabilitiesError, + onRetry: () { + _capabilitiesBloc.refresh(); + }, + ), + CustomLinearProgressIndicator( + visible: capabilitiesLoading, + ), + ], + 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 DrawerHeader( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (capabilitiesData != null) ...[ - Text( - capabilitiesData.capabilities.theming!.name, - style: DefaultTextStyle.of(context).style.copyWith( - color: Theme.of(context).appBarTheme.foregroundColor, - ), - ), - Flexible( - child: CachedURLImage( - url: capabilitiesData.capabilities.theming!.logo, - ), - ), - ] else ...[ - ExceptionWidget( - capabilitiesError, - onRetry: () { - _capabilitiesBloc.refresh(); + return Container(); + }, + ), + ExceptionWidget( + appsError, + onlyIcon: isQuickBar, + onRetry: () { + _appsBloc.refresh(); + }, + ), + CustomLinearProgressIndicator( + visible: appsLoading, + ), + if (appsData != null) ...[ + for (final appImplementation in appsData) ...[ + 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: () { + _appsBloc.setActiveApp(appImplementation.id); }, - ), - CustomLinearProgressIndicator( - visible: capabilitiesLoading, - ), - ], - 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; - } - } - }, + icon: AppImplementationIcon( + appImplementation: appImplementation, + unreadCount: unreadCount, + color: Theme.of(context).colorScheme.primary, ), ), - ], - ], - ), - ); - } - return Container(); - }, - ), - ExceptionWidget( - appsError, - onlyIcon: isQuickBar, - onRetry: () { - _appsBloc.refresh(); - }, - ), - CustomLinearProgressIndicator( - visible: appsLoading, - ), - if (appsData != null) ...[ - for (final appImplementation in appsData) ...[ - 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: () { - _appsBloc.setActiveApp(appImplementation.id); - }, - icon: Stack( - alignment: Alignment.bottomRight, - children: [ - Container( - margin: const EdgeInsets.all(5), - child: appImplementation.buildIcon( - context, - height: kAvatarSize, - width: kAvatarSize, - color: Theme.of(context).appBarTheme.foregroundColor, + ); + } + 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, ), ), - if (unreadCount > 0) ...[ - Text( - unreadCount.toString(), - style: TextStyle( - color: Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ], ], - ), + ], ), + leading: appImplementation.buildIcon(context), + minLeadingWidth: 0, + onTap: () { + _appsBloc.setActiveApp(appImplementation.id); + if (navigationMode == NavigationMode.drawer) { + // Don't pop when the drawer is always shown + Navigator.of(context).pop(); + } + }, ); - } - 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: () { - _appsBloc.setActiveApp(appImplementation.id); - if (navigationMode == NavigationMode.drawer) { - // Don't pop when the drawer is always shown - 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: capabilitiesData?.capabilities.theming?.logo != null - ? CachedURLImage( - url: capabilitiesData!.capabilities.theming!.logo, - ) - : null, - ) - : null, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (appsData != null && activeAppIDSnapshot.hasData) ...[ - Flexible( - child: Text( - appsData - .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: capabilitiesData?.capabilities.theming?.logo != null + ? CachedURLImage( + url: capabilitiesData!.capabilities.theming!.logo, + ) + : null, + ) + : null, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (appsData != null && activeAppIDSnapshot.hasData) ...[ + Flexible( + child: Text( + appsData + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .name(context), + ), ), - ), - ], - if (appsError != null) ...[ - const SizedBox( - width: 8, - ), - Icon( - Icons.error_outline, - size: 30, - color: Theme.of(context).colorScheme.onPrimary, - ), - ], - if (appsLoading) ...[ - const SizedBox( - width: 8, - ), - Expanded( - child: CustomLinearProgressIndicator( - color: Theme.of(context).appBarTheme.foregroundColor, + ], + if (appsError != null) ...[ + const SizedBox( + width: 8, ), - ), + Icon( + Icons.error_outline, + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), + ], + if (appsLoading) ...[ + 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 (notificationsAppData != null) ...[ + StreamBuilder( + stream: notificationsAppData.getUnreadCounter(_appsBloc), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + return IconButton( + key: Key('app-${notificationsAppData.id}'), + icon: AppImplementationIcon( + appImplementation: notificationsAppData, + 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 Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(notificationsAppData.name(context)), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall!, + ), + ], + ], + ), + ), + body: notificationsAppData.buildPage(context, _appsBloc), + ), + ), + ); + }, + ); + }, ), ], - ], - ), - actions: [ - IconButton( - icon: IntrinsicWidth( - child: AccountAvatar( - account: account, + 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) => AccountSettingsPage( + bloc: accountsBloc, + account: account, + ), ), - ), - ); - }, - ), - ], - ), - body: Row( - children: [ - if (navigationMode == NavigationMode.quickBar) ...[ - drawer, + ); + }, + ), ], - Expanded( - child: Column( - children: [ - ExceptionWidget( - appsError, - onRetry: () { - _appsBloc.refresh(); - }, - ), - if (appsData != null) ...[ - if (appsData.isEmpty) ...[ - Expanded( - child: Center( - child: Text( - AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, - textAlign: TextAlign.center, - ), - ), - ), - ] else ...[ - if (activeAppIDSnapshot.hasData) ...[ + ), + body: Row( + children: [ + if (navigationMode == NavigationMode.quickBar) ...[ + drawer, + ], + Expanded( + child: Column( + children: [ + ExceptionWidget( + appsError, + onRetry: () { + _appsBloc.refresh(); + }, + ), + if (appsData != null) ...[ + if (appsData.isEmpty) ...[ Expanded( - child: appsData - .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: appsData + .singleWhere((final a) => a.id == activeAppIDSnapshot.data!) + .buildPage(context, _appsBloc), + ), + ], ], ], ], - ], + ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - } - return Container(); - }, + ], + ), + ); + } + return Container(); + }, + ), ), ), ), diff --git a/packages/neon/lib/src/widgets/app_implementation_icon.dart b/packages/neon/lib/src/widgets/app_implementation_icon.dart new file mode 100644 index 00000000..48712624 --- /dev/null +++ b/packages/neon/lib/src/widgets/app_implementation_icon.dart @@ -0,0 +1,47 @@ +part of '../neon.dart'; + +class AppImplementationIcon extends StatelessWidget { + const AppImplementationIcon({ + required this.appImplementation, + this.unreadCount = 0, + this.color, + this.width = kAvatarSize, + this.height = kAvatarSize, + super.key, + }); + + final AppImplementation appImplementation; + + final int unreadCount; + + final Color? color; + + final double width; + + final double height; + + @override + Widget build(final BuildContext context) => Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + margin: const EdgeInsets.all(5), + child: appImplementation.buildIcon( + context, + height: height, + width: width, + color: color, + ), + ), + if (unreadCount > 0) ...[ + Text( + unreadCount.toString(), + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ], + ); +}