Browse Source

Merge pull request #121 from provokateurin/feature/separate-notifications

Separate notifications from other apps
pull/122/head
Kate 2 years ago committed by GitHub
parent
commit
23a6253480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/neon/integration_test/screenshot_test.dart
  2. 2
      packages/neon/lib/src/apps/news/app.dart
  3. 2
      packages/neon/lib/src/apps/notifications/app.dart
  4. 22
      packages/neon/lib/src/blocs/accounts.dart
  5. 66
      packages/neon/lib/src/blocs/apps.dart
  6. 10
      packages/neon/lib/src/blocs/apps.rxb.g.dart
  7. 3
      packages/neon/lib/src/neon.dart
  8. 742
      packages/neon/lib/src/pages/home.dart
  9. 47
      packages/neon/lib/src/widgets/app_implementation_icon.dart

3
packages/neon/integration_test/screenshot_test.dart

@ -486,7 +486,8 @@ Future main() async {
), ),
); );
await prepareScreenshot(tester, binding); 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.pumpAndSettle();
await tester.pump(); await tester.pump();

2
packages/neon/lib/src/apps/news/app.dart

@ -71,5 +71,5 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
); );
@override @override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter; BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter;
} }

2
packages/neon/lib/src/apps/notifications/app.dart

@ -40,6 +40,6 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
); );
@override @override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) =>
appsBloc.getAppBloc<NotificationsBloc>(this).unreadCounter; appsBloc.getAppBloc<NotificationsBloc>(this).unreadCounter;
} }

22
packages/neon/lib/src/blocs/accounts.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:neon/src/blocs/apps.dart'; 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_details.dart';
import 'package:neon/src/blocs/user_status.dart'; import 'package:neon/src/blocs/user_status.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
@ -127,18 +128,30 @@ class AccountsBloc extends $AccountsBloc {
); );
AppsBloc getAppsBloc(final Account account) { AppsBloc getAppsBloc(final Account account) {
if (_accountsAppsBlocs[account.id] != null) { if (_appsBlocs[account.id] != null) {
return _accountsAppsBlocs[account.id]!; return _appsBlocs[account.id]!;
} }
return _accountsAppsBlocs[account.id] = AppsBloc( return _appsBlocs[account.id] = AppsBloc(
_requestManager, _requestManager,
getCapabilitiesBloc(account),
this, this,
account, account,
_allAppImplementations, _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) { UserDetailsBloc getUserDetailsBloc(final Account account) {
if (_userDetailsBlocs[account.id] != null) { if (_userDetailsBlocs[account.id] != null) {
return _userDetailsBlocs[account.id]!; return _userDetailsBlocs[account.id]!;
@ -176,7 +189,8 @@ class AccountsBloc extends $AccountsBloc {
late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null); late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null);
late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]); late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]);
final _accountsAppsBlocs = <String, AppsBloc>{}; final _appsBlocs = <String, AppsBloc>{};
final _capabilitiesBlocs = <String, CapabilitiesBloc>{};
final _userDetailsBlocs = <String, UserDetailsBloc>{}; final _userDetailsBlocs = <String, UserDetailsBloc>{};
final _userStatusBlocs = <String, UserStatusBloc>{}; final _userStatusBlocs = <String, UserStatusBloc>{};

66
packages/neon/lib/src/blocs/apps.dart

@ -1,7 +1,8 @@
import 'dart:async'; 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/accounts.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart'; import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -22,6 +23,8 @@ abstract class AppsBlocStates {
BehaviorSubject<Result<List<AppImplementation>>> get appImplementations; BehaviorSubject<Result<List<AppImplementation>>> get appImplementations;
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation;
BehaviorSubject<String?> get activeAppID; BehaviorSubject<String?> get activeAppID;
} }
@ -29,6 +32,7 @@ abstract class AppsBlocStates {
class AppsBloc extends $AppsBloc { class AppsBloc extends $AppsBloc {
AppsBloc( AppsBloc(
this._requestManager, this._requestManager,
this._capabilitiesBloc,
this._accountsBloc, this._accountsBloc,
this._account, this._account,
this._allAppImplementations, this._allAppImplementations,
@ -40,28 +44,31 @@ class AppsBloc extends $AppsBloc {
if (_activeAppSubject.valueOrNull != appId) { if (_activeAppSubject.valueOrNull != appId) {
_activeAppSubject.add(appId); _activeAppSubject.add(appId);
} }
} else if (appId == 'notifications') {
// TODO: Open notifications page
} else { } else {
debugPrint('App $appId not found'); throw Exception('App $appId not found');
} }
}); });
_appsSubject.listen((final result) { _appsSubject.listen((final result) {
if (result is ResultLoading) { if (result is ResultLoading) {
_appImplementationsSubject.add(Result.loading()); _appImplementationsSubject.add(Result.loading());
} else if (result is ResultError) { } else if (result is ResultError<List<NextcloudApp>>) {
_appImplementationsSubject.add(Result.error((result as ResultError).error)); _appImplementationsSubject.add(Result.error(result.error));
} else if (result is ResultSuccess) { } else if (result is ResultSuccess<List<NextcloudApp>>) {
_appImplementationsSubject.add( _appImplementationsSubject.add(
Result.success(_filteredAppImplementations((result as ResultSuccess<List<NextcloudApp>>).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<List<NextcloudApp>>) {
_appImplementationsSubject.add( _appImplementationsSubject.add(
ResultCached(_filteredAppImplementations((result as ResultCached<List<NextcloudApp>>).data)), ResultCached(_filteredAppImplementations(result.data.map((final a) => a.id).toList())),
); );
} }
final appImplementations = final appImplementations = result.data != null
result.data != null ? _filteredAppImplementations(result.data!) : <AppImplementation>[]; ? _filteredAppImplementations(result.data!.map((final a) => a.id).toList())
: <AppImplementation>[];
if (result.data != null) { if (result.data != null) {
final options = _accountsBloc.getOptions(_account); 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<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(Result.error(result.error));
} else if (result is ResultSuccess<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(
Result.success(
result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null,
),
);
} else if (result is ResultCached<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(
ResultCached(result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null),
);
}
});
_loadApps(); _loadApps();
} }
final _extraApps = ['notifications']; T? _findAppImplementation<T extends AppImplementation>(final String id) {
final matches = _filteredAppImplementations([id]);
if (matches.isNotEmpty) {
return matches.single as T;
}
List<AppImplementation> _filteredAppImplementations(final List<NextcloudApp> apps) { return null;
final appIds = apps.map((final a) => a.id).toList();
return _allAppImplementations.where((final a) => appIds.contains(a.id) || _extraApps.contains(a.id)).toList();
} }
List<AppImplementation> _filteredAppImplementations(final List<String> appIds) =>
_allAppImplementations.where((final a) => appIds.contains(a.id)).toList();
void _loadApps() { void _loadApps() {
_requestManager _requestManager
.wrapNextcloud<List<NextcloudApp>, CoreNavigationApps>( .wrapNextcloud<List<NextcloudApp>, CoreNavigationApps>(
@ -107,12 +137,14 @@ class AppsBloc extends $AppsBloc {
} }
final RequestManager _requestManager; final RequestManager _requestManager;
final CapabilitiesBloc _capabilitiesBloc;
final AccountsBloc _accountsBloc; final AccountsBloc _accountsBloc;
final Account _account; final Account _account;
final List<AppImplementation> _allAppImplementations; final List<AppImplementation> _allAppImplementations;
final _appsSubject = BehaviorSubject<Result<List<NextcloudApp>>>(); final _appsSubject = BehaviorSubject<Result<List<NextcloudApp>>>();
final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>(); final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>();
final _notificationsAppImplementationSubject = BehaviorSubject<Result<NotificationsApp?>>();
late final _activeAppSubject = BehaviorSubject<String?>(); late final _activeAppSubject = BehaviorSubject<String?>();
final Map<String, RxBlocBase> _blocs = {}; final Map<String, RxBlocBase> _blocs = {};
@ -128,6 +160,8 @@ class AppsBloc extends $AppsBloc {
@override @override
void dispose() { void dispose() {
unawaited(_appsSubject.close()); unawaited(_appsSubject.close());
unawaited(_appImplementationsSubject.close());
unawaited(_notificationsAppImplementationSubject.close());
unawaited(_activeAppSubject.close()); unawaited(_activeAppSubject.close());
for (final key in _blocs.keys) { for (final key in _blocs.keys) {
_blocs[key]!.dispose(); _blocs[key]!.dispose();
@ -142,6 +176,10 @@ class AppsBloc extends $AppsBloc {
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState() => _appImplementationsSubject; _mapToAppImplementationsState() => _appImplementationsSubject;
@override
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState() =>
_notificationsAppImplementationSubject;
@override @override
BehaviorSubject<String?> _mapToActiveAppIDState() => _activeAppSubject; BehaviorSubject<String?> _mapToActiveAppIDState() => _activeAppSubject;
} }

10
packages/neon/lib/src/blocs/apps.rxb.g.dart

@ -32,6 +32,11 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
late final BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> late final BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_appImplementationsState = _mapToAppImplementationsState(); _appImplementationsState = _mapToAppImplementationsState();
/// The state of [notificationsAppImplementation] implemented in
/// [_mapToNotificationsAppImplementationState]
late final BehaviorSubject<Result<NotificationsApp?>> _notificationsAppImplementationState =
_mapToNotificationsAppImplementationState();
/// The state of [activeAppID] implemented in [_mapToActiveAppIDState] /// The state of [activeAppID] implemented in [_mapToActiveAppIDState]
late final BehaviorSubject<String?> _activeAppIDState = _mapToActiveAppIDState(); late final BehaviorSubject<String?> _activeAppIDState = _mapToActiveAppIDState();
@ -48,6 +53,9 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> get appImplementations => BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> get appImplementations =>
_appImplementationsState; _appImplementationsState;
@override
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation => _notificationsAppImplementationState;
@override @override
BehaviorSubject<String?> get activeAppID => _activeAppIDState; BehaviorSubject<String?> get activeAppID => _activeAppIDState;
@ -56,6 +64,8 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState(); _mapToAppImplementationsState();
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState();
BehaviorSubject<String?> _mapToActiveAppIDState(); BehaviorSubject<String?> _mapToActiveAppIDState();
@override @override

3
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; import 'package:xdg_directories/xdg_directories.dart' as xdg;
part 'app.dart'; part 'app.dart';
part 'pages/account_settings.dart';
part 'pages/home.dart'; part 'pages/home.dart';
part 'pages/login.dart'; part 'pages/login.dart';
part 'pages/account_settings.dart';
part 'pages/nextcloud_app_settings.dart'; part 'pages/nextcloud_app_settings.dart';
part 'pages/settings.dart'; part 'pages/settings.dart';
part 'platform/abstract.dart'; part 'platform/abstract.dart';
@ -88,6 +88,7 @@ part 'utils/validators.dart';
part 'widgets/account_avatar.dart'; part 'widgets/account_avatar.dart';
part 'widgets/account_settings_tile.dart'; part 'widgets/account_settings_tile.dart';
part 'widgets/account_tile.dart'; part 'widgets/account_tile.dart';
part 'widgets/app_implementation_icon.dart';
part 'widgets/cached_api_image.dart'; part 'widgets/cached_api_image.dart';
part 'widgets/cached_image.dart'; part 'widgets/cached_image.dart';
part 'widgets/cached_url_image.dart'; part 'widgets/cached_url_image.dart';

742
packages/neon/lib/src/pages/home.dart

@ -30,11 +30,8 @@ class _HomePageState extends State<HomePage> {
_globalOptions = Provider.of<GlobalOptions>(context, listen: false); _globalOptions = Provider.of<GlobalOptions>(context, listen: false);
_appsBloc = RxBlocProvider.of<AccountsBloc>(context).getAppsBloc(widget.account); _appsBloc = RxBlocProvider.of<AccountsBloc>(context).getAppsBloc(widget.account);
_capabilitiesBloc = RxBlocProvider.of<AccountsBloc>(context).getCapabilitiesBloc(widget.account);
_capabilitiesBloc = CapabilitiesBloc(
Provider.of<RequestManager>(context, listen: false),
widget.account.client,
);
_capabilitiesBloc.capabilities.listen((final result) async { _capabilitiesBloc.capabilities.listen((final result) async {
if (result.data != null) { if (result.data != null) {
widget.onThemeChanged(result.data!.capabilities.theming!); widget.onThemeChanged(result.data!.capabilities.theming!);
@ -162,411 +159,448 @@ class _HomePageState extends State<HomePage> {
final appsLoading, final appsLoading,
final _, final _,
) => ) =>
RxBlocBuilder<AppsBloc, String?>( StandardRxResultBuilder<AppsBloc, NotificationsApp?>(
bloc: _appsBloc, bloc: _appsBloc,
state: (final bloc) => bloc.activeAppID, state: (final bloc) => bloc.notificationsAppImplementation,
builder: ( builder: (
final context, final context,
final activeAppIDSnapshot, final notificationsAppData,
final notificationsAppError,
final notificationsAppLoading,
final _, final _,
) => ) =>
RxBlocBuilder<AccountsBloc, List<Account>>( RxBlocBuilder<AppsBloc, String?>(
bloc: accountsBloc, bloc: _appsBloc,
state: (final bloc) => bloc.accounts, state: (final bloc) => bloc.activeAppID,
builder: ( builder: (
final context, final context,
final accountsSnapshot, final activeAppIDSnapshot,
final _, final _,
) => ) =>
OptionBuilder<NavigationMode>( RxBlocBuilder<AccountsBloc, List<Account>>(
option: _globalOptions.navigationMode, bloc: accountsBloc,
builder: (final context, final navigationMode) => WillPopScope( state: (final bloc) => bloc.accounts,
onWillPop: () async { builder: (
if (_scaffoldKey.currentState!.isDrawerOpen) { final context,
Navigator.pop(context); final accountsSnapshot,
return true; final _,
} ) =>
OptionBuilder<NavigationMode>(
option: _globalOptions.navigationMode,
builder: (final context, final navigationMode) => WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context);
return true;
}
_scaffoldKey.currentState!.openDrawer(); _scaffoldKey.currentState!.openDrawer();
return false; return false;
}, },
child: Builder( child: Builder(
builder: (final context) { builder: (final context) {
if (accountsSnapshot.hasData) { if (accountsSnapshot.hasData) {
final accounts = accountsSnapshot.data!; final accounts = accountsSnapshot.data!;
final account = accounts.singleWhere((final account) => account.id == widget.account.id); final account = accounts.singleWhere((final account) => account.id == widget.account.id);
final isQuickBar = navigationMode == NavigationMode.quickBar; final isQuickBar = navigationMode == NavigationMode.quickBar;
final drawer = Drawer( final drawer = Drawer(
width: isQuickBar ? kQuickBarWidth : null, width: isQuickBar ? kQuickBarWidth : null,
child: Container( child: Container(
padding: isQuickBar ? const EdgeInsets.all(5) : null, padding: isQuickBar ? const EdgeInsets.all(5) : null,
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
child: Scrollbar( child: Scrollbar(
child: ListView( child: ListView(
// Needed for the drawer header to also render in the statusbar // Needed for the drawer header to also render in the statusbar
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: [ children: [
Builder( Builder(
builder: (final context) { builder: (final context) {
if (accountsSnapshot.hasData) { if (accountsSnapshot.hasData) {
if (isQuickBar) { if (isQuickBar) {
return Column( return Column(
children: [ children: [
if (accounts.length != 1) ...[ if (accounts.length != 1) ...[
for (final account in accounts) ...[ for (final account in accounts) ...[
Container( Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
vertical: 5, vertical: 5,
), ),
child: Tooltip( child: Tooltip(
message: account.client.humanReadableID, message: account.client.humanReadableID,
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
accountsBloc.setActiveAccount(account); accountsBloc.setActiveAccount(account);
}, },
icon: IntrinsicHeight( icon: IntrinsicHeight(
child: AccountAvatar( child: AccountAvatar(
account: account, 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( Flexible(
height: 5, child: CachedURLImage(
color: Theme.of(context).appBarTheme.foregroundColor, url: capabilitiesData.capabilities.theming!.logo,
),
), ),
), ] else ...[
ExceptionWidget(
capabilitiesError,
onRetry: () {
_capabilitiesBloc.refresh();
},
),
CustomLinearProgressIndicator(
visible: capabilitiesLoading,
),
],
if (accounts.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: widget.account.id,
items: accounts
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
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( return Container();
decoration: BoxDecoration( },
color: Theme.of(context).colorScheme.primary, ),
), ExceptionWidget(
child: Column( appsError,
crossAxisAlignment: CrossAxisAlignment.start, onlyIcon: isQuickBar,
mainAxisAlignment: MainAxisAlignment.spaceBetween, onRetry: () {
children: [ _appsBloc.refresh();
if (capabilitiesData != null) ...[ },
Text( ),
capabilitiesData.capabilities.theming!.name, CustomLinearProgressIndicator(
style: DefaultTextStyle.of(context).style.copyWith( visible: appsLoading,
color: Theme.of(context).appBarTheme.foregroundColor, ),
), if (appsData != null) ...[
), for (final appImplementation in appsData) ...[
Flexible( StreamBuilder<int>(
child: CachedURLImage( stream: appImplementation.getUnreadCounter(_appsBloc) ??
url: capabilitiesData.capabilities.theming!.logo, BehaviorSubject<int>.seeded(0),
), builder: (final context, final unreadCounterSnapshot) {
), final unreadCount = unreadCounterSnapshot.data ?? 0;
] else ...[ if (isQuickBar) {
ExceptionWidget( return Tooltip(
capabilitiesError, message: appImplementation.name(context),
onRetry: () { child: IconButton(
_capabilitiesBloc.refresh(); onPressed: () {
_appsBloc.setActiveApp(appImplementation.id);
}, },
), icon: AppImplementationIcon(
CustomLinearProgressIndicator( appImplementation: appImplementation,
visible: capabilitiesLoading, unreadCount: unreadCount,
), color: Theme.of(context).colorScheme.primary,
],
if (accounts.length != 1) ...[
DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: widget.account.id,
items: accounts
.map<DropdownMenuItem<String>>(
(final account) => DropdownMenuItem<String>(
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 ListTile(
); key: Key('app-${appImplementation.id}'),
} title: Row(
return Container(); mainAxisAlignment: MainAxisAlignment.spaceBetween,
}, children: [
), Text(appImplementation.name(context)),
ExceptionWidget( if (unreadCount > 0) ...[
appsError, Text(
onlyIcon: isQuickBar, unreadCount.toString(),
onRetry: () { style: TextStyle(
_appsBloc.refresh(); color: Theme.of(context).colorScheme.primary,
}, fontWeight: FontWeight.bold,
), fontSize: 14,
CustomLinearProgressIndicator(
visible: appsLoading,
),
if (appsData != null) ...[
for (final appImplementation in appsData) ...[
StreamBuilder<int>(
stream: appImplementation.getUnreadCounter(_appsBloc) ??
BehaviorSubject<int>.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,
), ),
), ),
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) ...[
if (isQuickBar) ...[ IconButton(
IconButton( icon: Icon(
icon: Icon( Icons.settings,
Icons.settings, color: Theme.of(context).appBarTheme.foregroundColor,
color: Theme.of(context).appBarTheme.foregroundColor, ),
onPressed: _openSettings,
), ),
onPressed: _openSettings, ] else ...[
), ListTile(
] else ...[ key: const Key('settings'),
ListTile( title: Text(AppLocalizations.of(context).settings),
key: const Key('settings'), leading: const Icon(Icons.settings),
title: Text(AppLocalizations.of(context).settings), minLeadingWidth: 0,
leading: const Icon(Icons.settings), onTap: () async {
minLeadingWidth: 0, if (navigationMode == NavigationMode.drawer) {
onTap: () async { Navigator.of(context).pop();
if (navigationMode == NavigationMode.drawer) { }
Navigator.of(context).pop(); await _openSettings();
} },
await _openSettings(); ),
}, ],
),
], ],
], ),
), ),
), );
);
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Row( body: Row(
children: [ children: [
if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[
drawer, drawer,
], ],
Expanded( Expanded(
child: Scaffold( child: Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
drawer: navigationMode == NavigationMode.drawer ? drawer : null, drawer: navigationMode == NavigationMode.drawer ? drawer : null,
appBar: AppBar( appBar: AppBar(
scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null, scrolledUnderElevation: navigationMode != NavigationMode.drawer ? 0 : null,
automaticallyImplyLeading: navigationMode == NavigationMode.drawer, automaticallyImplyLeading: navigationMode == NavigationMode.drawer,
leadingWidth: isQuickBar ? kQuickBarWidth : null, leadingWidth: isQuickBar ? kQuickBarWidth : null,
leading: isQuickBar leading: isQuickBar
? Container( ? Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
child: capabilitiesData?.capabilities.theming?.logo != null child: capabilitiesData?.capabilities.theming?.logo != null
? CachedURLImage( ? CachedURLImage(
url: capabilitiesData!.capabilities.theming!.logo, url: capabilitiesData!.capabilities.theming!.logo,
) )
: null, : null,
) )
: null, : null,
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
if (appsData != null && activeAppIDSnapshot.hasData) ...[ if (appsData != null && activeAppIDSnapshot.hasData) ...[
Flexible( Flexible(
child: Text( child: Text(
appsData appsData
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!) .singleWhere((final a) => a.id == activeAppIDSnapshot.data!)
.name(context), .name(context),
),
), ),
), ],
], if (appsError != null) ...[
if (appsError != null) ...[ const SizedBox(
const SizedBox( width: 8,
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,
), ),
), 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( actions: [
account.client.humanReadableID, if (notificationsAppData != null) ...[
style: Theme.of(context).textTheme.bodySmall!, StreamBuilder<int>(
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),
),
),
);
},
);
},
), ),
], ],
], IconButton(
), icon: IntrinsicWidth(
actions: [ child: AccountAvatar(
IconButton( account: account,
icon: IntrinsicWidth( ),
child: AccountAvatar(
account: account,
), ),
), onPressed: () async {
onPressed: () async { await Navigator.of(context).push(
await Navigator.of(context).push( MaterialPageRoute(
MaterialPageRoute( builder: (final context) => AccountSettingsPage(
builder: (final context) => AccountSettingsPage( bloc: accountsBloc,
bloc: accountsBloc, account: account,
account: account, ),
), ),
), );
); },
}, ),
),
],
),
body: Row(
children: [
if (navigationMode == NavigationMode.quickBar) ...[
drawer,
], ],
Expanded( ),
child: Column( body: Row(
children: [ children: [
ExceptionWidget( if (navigationMode == NavigationMode.quickBar) ...[
appsError, drawer,
onRetry: () { ],
_appsBloc.refresh(); Expanded(
}, child: Column(
), children: [
if (appsData != null) ...[ ExceptionWidget(
if (appsData.isEmpty) ...[ appsError,
Expanded( onRetry: () {
child: Center( _appsBloc.refresh();
child: Text( },
AppLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, ),
textAlign: TextAlign.center, if (appsData != null) ...[
), if (appsData.isEmpty) ...[
),
),
] else ...[
if (activeAppIDSnapshot.hasData) ...[
Expanded( Expanded(
child: appsData child: Center(
.singleWhere((final a) => a.id == activeAppIDSnapshot.data!) child: Text(
.buildPage(context, _appsBloc), 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(); },
}, ),
), ),
), ),
), ),

47
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,
),
),
],
],
);
}
Loading…
Cancel
Save