diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index 8f4ea9ff..048c8f32 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -26,6 +26,7 @@ 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/app_bar.dart'; import 'package:neon/src/widgets/drawer.dart'; import 'package:neon/src/widgets/drawer_destination.dart'; import 'package:nextcloud/nextcloud.dart'; diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 6758aeb5..56a937ad 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -11,16 +11,13 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -// ignore: prefer_mixin class _HomePageState extends State { final _scaffoldKey = GlobalKey(); - final drawerScrollController = ScrollController(); late Account _account; late GlobalOptions _globalOptions; late AccountsBloc _accountsBloc; late AppsBloc _appsBloc; - late CapabilitiesBloc _capabilitiesBloc; @override void initState() { @@ -29,18 +26,6 @@ class _HomePageState extends State { _accountsBloc = Provider.of(context, listen: false); _account = _accountsBloc.activeAccount.value!; _appsBloc = _accountsBloc.activeAppsBloc; - _capabilitiesBloc = _accountsBloc.activeCapabilitiesBloc; - - _appsBloc.openNotifications.listen((final _) async { - final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; - if (notificationsAppImplementation != null) { - await _openNotifications( - notificationsAppImplementation.data!, - _accountsBloc.accounts.value, - _accountsBloc.activeAccount.value!, - ); - } - }); _appsBloc.appVersions.listen((final values) { if (values == null || !mounted) { @@ -108,219 +93,79 @@ class _HomePageState extends State { ); } - Future _openNotifications( - final NotificationsAppInterface app, - final List accounts, - final Account account, - ) async { - final page = Scaffold( - resizeToAvoidBottomInset: false, - 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.page, - ); - - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => Provider( - create: (final context) => app.getBloc(account), - child: page, - ), - ), - ); - } - @override - void dispose() { - drawerScrollController.dispose(); - super.dispose(); - } - - @override - Widget build(final BuildContext context) => ResultBuilder.behaviorSubject( - stream: _capabilitiesBloc.capabilities, - builder: (final context, final capabilities) => ResultBuilder>.behaviorSubject( - stream: _appsBloc.appImplementations, - builder: (final context, final appImplementations) => - ResultBuilder.behaviorSubject( - stream: _appsBloc.notificationsAppImplementation, - builder: (final context, final notificationsAppImplementation) => StreamBuilder( - stream: _appsBloc.activeAppID, - builder: (final context, final activeAppIDSnapshot) => StreamBuilder>( - stream: _accountsBloc.accounts, - builder: (final context, final accountsSnapshot) => OptionBuilder( - option: _globalOptions.navigationMode, - builder: (final context, final navigationMode) { - final accounts = accountsSnapshot.data; - final account = accounts?.find(_account.id); - if (accounts == null || account == null) { - return const Scaffold(); - } - - final drawerAlwaysVisible = navigationMode == NavigationMode.drawerAlwaysVisible; - - const drawer = NeonDrawer(); - final appBar = AppBar( - automaticallyImplyLeading: !drawerAlwaysVisible, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (appImplementations.data != null && activeAppIDSnapshot.hasData) ...[ - Flexible( - child: Text( - appImplementations.data!.find(activeAppIDSnapshot.data!)!.name(context), - ), - ), - ], - if (appImplementations.hasError) ...[ - const SizedBox( - width: 8, - ), - NeonException( - appImplementations.error, - onRetry: _appsBloc.refresh, - onlyIcon: true, - ), - ], - if (appImplementations.isLoading) ...[ - const SizedBox( - width: 8, - ), - Expanded( - child: NeonLinearProgressIndicator( - color: Theme.of(context).appBarTheme.foregroundColor, - ), - ), - ], - ], - ), - if (accounts.length > 1) ...[ - Text( - account.client.humanReadableID, - style: Theme.of(context).textTheme.bodySmall, + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( + stream: _appsBloc.appImplementations, + builder: (final context, final appImplementations) => StreamBuilder( + stream: _appsBloc.activeAppID, + builder: (final context, final activeAppIDSnapshot) => OptionBuilder( + option: _globalOptions.navigationMode, + builder: (final context, final navigationMode) { + final drawerAlwaysVisible = navigationMode == NavigationMode.drawerAlwaysVisible; + + const drawer = NeonDrawer(); + const appBar = NeonAppBar(); + + Widget body = Builder( + builder: (final context) => Column( + children: [ + if (appImplementations.data != null) ...[ + if (appImplementations.data!.isEmpty) ...[ + Expanded( + child: Center( + child: Text( + AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, + textAlign: TextAlign.center, ), - ], - ], - ), - actions: [ - if (notificationsAppImplementation.data != null) ...[ - StreamBuilder( - stream: notificationsAppImplementation.data! - .getUnreadCounter(notificationsAppImplementation.data!.getBloc(account)), - builder: (final context, final unreadCounterSnapshot) { - final unreadCount = unreadCounterSnapshot.data ?? 0; - return IconButton( - key: Key('app-${notificationsAppImplementation.data!.id}'), - onPressed: () async { - await _openNotifications( - notificationsAppImplementation.data!, - accounts, - account, - ); - }, - tooltip: AppLocalizations.of(context) - .appImplementationName(notificationsAppImplementation.data!.id), - icon: NeonAppImplementationIcon( - appImplementation: notificationsAppImplementation.data!, - unreadCount: unreadCount, - color: unreadCount > 0 - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, - size: const Size.square(kAvatarSize * 2 / 3), - ), - ); - }, - ), - ], - IconButton( - onPressed: () { - AccountSettingsRoute(accountid: account.id).go(context); - }, - tooltip: AppLocalizations.of(context).settingsAccount, - icon: NeonUserAvatar( - account: account, ), ), - ], - ); - - Widget body = Builder( - builder: (final context) => 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, - ), - ], - ], - ], - ], - ), - ); - - body = MultiProvider( - providers: _appsBloc.appBlocProviders, - child: Scaffold( - key: _scaffoldKey, - resizeToAvoidBottomInset: false, - drawer: !drawerAlwaysVisible ? drawer : null, - appBar: appBar, - body: body, - ), - ); - - if (drawerAlwaysVisible) { - body = Row( - children: [ - drawer, + ] else ...[ + if (activeAppIDSnapshot.hasData) ...[ Expanded( - child: body, + child: appImplementations.data!.find(activeAppIDSnapshot.data!)!.page, ), ], - ); - } - - return WillPopScope( - onWillPop: () async { - if (_scaffoldKey.currentState!.isDrawerOpen) { - Navigator.pop(context); - return true; - } + ], + ], + ], + ), + ); + + body = MultiProvider( + providers: _appsBloc.appBlocProviders, + child: Scaffold( + key: _scaffoldKey, + resizeToAvoidBottomInset: false, + drawer: !drawerAlwaysVisible ? drawer : null, + appBar: appBar, + body: body, + ), + ); - _scaffoldKey.currentState!.openDrawer(); - return false; - }, + if (drawerAlwaysVisible) { + body = Row( + children: [ + drawer, + Expanded( child: body, - ); - }, - ), - ), - ), + ), + ], + ); + } + + return WillPopScope( + onWillPop: () async { + if (_scaffoldKey.currentState!.isDrawerOpen) { + Navigator.pop(context); + return true; + } + + _scaffoldKey.currentState!.openDrawer(); + return false; + }, + child: body, + ); + }, ), ), ); diff --git a/packages/neon/neon/lib/src/widgets/app_bar.dart b/packages/neon/neon/lib/src/widgets/app_bar.dart new file mode 100644 index 00000000..dddb6d25 --- /dev/null +++ b/packages/neon/neon/lib/src/widgets/app_bar.dart @@ -0,0 +1,196 @@ +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:provider/provider.dart'; + +@internal +class NeonAppBar extends StatelessWidget implements PreferredSizeWidget { + const NeonAppBar({super.key}); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(final BuildContext context) { + final accountsBloc = Provider.of(context, listen: false); + final accounts = accountsBloc.accounts.value; + final account = accountsBloc.activeAccount.value!; + final appsBloc = accountsBloc.activeAppsBloc; + + return ResultBuilder>.behaviorSubject( + stream: appsBloc.appImplementations, + builder: (final context, final appImplementations) => StreamBuilder( + stream: appsBloc.activeAppID, + builder: (final context, final activeAppIDSnapshot) => AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (appImplementations.hasData && activeAppIDSnapshot.hasData) ...[ + Flexible( + child: Text( + appImplementations.requireData.find(activeAppIDSnapshot.data!)!.name(context), + ), + ), + ], + if (appImplementations.hasError) ...[ + const SizedBox( + width: 8, + ), + NeonException( + appImplementations.error, + onRetry: appsBloc.refresh, + onlyIcon: true, + ), + ], + if (appImplementations.isLoading) ...[ + const SizedBox( + width: 8, + ), + Expanded( + child: NeonLinearProgressIndicator( + color: Theme.of(context).appBarTheme.foregroundColor, + ), + ), + ], + ], + ), + if (accounts.length > 1) ...[ + Text( + account.client.humanReadableID, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ], + ), + actions: [ + const NotificationIconButton(), + IconButton( + onPressed: () { + AccountSettingsRoute(accountid: account.id).go(context); + }, + tooltip: AppLocalizations.of(context).settingsAccount, + icon: NeonUserAvatar( + account: account, + ), + ), + ], + ), + ), + ); + } +} + +@internal +class NotificationIconButton extends StatefulWidget { + const NotificationIconButton({ + super.key, + }); + + @override + State createState() => _NotificationIconButtonState(); +} + +class _NotificationIconButtonState extends State { + late AccountsBloc _accountsBloc; + late AppsBloc _appsBloc; + late List _accounts; + late Account _account; + late StreamSubscription notificationSubscription; + + @override + void initState() { + super.initState(); + _accountsBloc = Provider.of(context, listen: false); + _appsBloc = _accountsBloc.activeAppsBloc; + _accounts = _accountsBloc.accounts.value; + _account = _accountsBloc.activeAccount.value!; + + notificationSubscription = _appsBloc.openNotifications.listen((final _) async { + final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; + if (notificationsAppImplementation != null && notificationsAppImplementation.hasData) { + await _openNotifications(notificationsAppImplementation.data!); + } + }); + } + + @override + void dispose() { + unawaited(notificationSubscription.cancel()); + + super.dispose(); + } + + // TODO: migrate to go_router with a separate page + Future _openNotifications( + final NotificationsAppInterface app, + ) async { + final page = Scaffold( + resizeToAvoidBottomInset: false, + 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.page, + ); + + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => Provider( + create: (final context) => app.getBloc(_account), + child: page, + ), + ), + ); + } + + @override + Widget build(final BuildContext context) => ResultBuilder.behaviorSubject( + stream: _appsBloc.notificationsAppImplementation, + builder: (final context, final notificationsAppImplementation) { + if (!notificationsAppImplementation.hasData) { + return const SizedBox.shrink(); + } + + final notificationsImplementationData = notificationsAppImplementation.data!; + final notificationBloc = notificationsImplementationData.getBloc(_account); + + return StreamBuilder( + stream: notificationsImplementationData.getUnreadCounter(notificationBloc), + builder: (final context, final unreadCounterSnapshot) { + final unreadCount = unreadCounterSnapshot.data ?? 0; + return IconButton( + key: Key('app-${notificationsImplementationData.id}'), + onPressed: () async { + await _openNotifications(notificationsImplementationData); + }, + tooltip: AppLocalizations.of(context).appImplementationName(notificationsImplementationData.id), + icon: NeonAppImplementationIcon( + appImplementation: notificationsImplementationData, + unreadCount: unreadCount, + color: unreadCount > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onBackground, + size: const Size.square(kAvatarSize * 2 / 3), + ), + ); + }, + ); + }, + ); +}