Browse Source

neon: Separate notifications from other apps

pull/121/head
jld3103 2 years ago
parent
commit
627e8890b7
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  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 switchPage(tester, 'app-notifications');
await tester.tap(find.byKey(const Key('app-notifications')));
await tester.pumpAndSettle();
await tester.pumpAndSettle();
await tester.pump();

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

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

22
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<Account?>.seeded(null);
late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]);
final _accountsAppsBlocs = <String, AppsBloc>{};
final _appsBlocs = <String, AppsBloc>{};
final _capabilitiesBlocs = <String, CapabilitiesBloc>{};
final _userDetailsBlocs = <String, UserDetailsBloc>{};
final _userStatusBlocs = <String, UserStatusBloc>{};

66
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<Result<List<AppImplementation>>> get appImplementations;
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation;
BehaviorSubject<String?> 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<List<NextcloudApp>>) {
_appImplementationsSubject.add(Result.error(result.error));
} else if (result is ResultSuccess<List<NextcloudApp>>) {
_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(
ResultCached(_filteredAppImplementations((result as ResultCached<List<NextcloudApp>>).data)),
ResultCached(_filteredAppImplementations(result.data.map((final a) => a.id).toList())),
);
}
final appImplementations =
result.data != null ? _filteredAppImplementations(result.data!) : <AppImplementation>[];
final appImplementations = result.data != null
? _filteredAppImplementations(result.data!.map((final a) => a.id).toList())
: <AppImplementation>[];
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<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();
}
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) {
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<AppImplementation> _filteredAppImplementations(final List<String> appIds) =>
_allAppImplementations.where((final a) => appIds.contains(a.id)).toList();
void _loadApps() {
_requestManager
.wrapNextcloud<List<NextcloudApp>, CoreNavigationApps>(
@ -107,12 +137,14 @@ class AppsBloc extends $AppsBloc {
}
final RequestManager _requestManager;
final CapabilitiesBloc _capabilitiesBloc;
final AccountsBloc _accountsBloc;
final Account _account;
final List<AppImplementation> _allAppImplementations;
final _appsSubject = BehaviorSubject<Result<List<NextcloudApp>>>();
final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>();
final _notificationsAppImplementationSubject = BehaviorSubject<Result<NotificationsApp?>>();
late final _activeAppSubject = BehaviorSubject<String?>();
final Map<String, RxBlocBase> _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<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState() => _appImplementationsSubject;
@override
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState() =>
_notificationsAppImplementationSubject;
@override
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>>>>
_appImplementationsState = _mapToAppImplementationsState();
/// The state of [notificationsAppImplementation] implemented in
/// [_mapToNotificationsAppImplementationState]
late final BehaviorSubject<Result<NotificationsApp?>> _notificationsAppImplementationState =
_mapToNotificationsAppImplementationState();
/// The state of [activeAppID] implemented in [_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 =>
_appImplementationsState;
@override
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation => _notificationsAppImplementationState;
@override
BehaviorSubject<String?> get activeAppID => _activeAppIDState;
@ -56,6 +64,8 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState();
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState();
BehaviorSubject<String?> _mapToActiveAppIDState();
@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;
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';

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

@ -30,11 +30,8 @@ class _HomePageState extends State<HomePage> {
_globalOptions = Provider.of<GlobalOptions>(context, listen: false);
_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 {
if (result.data != null) {
widget.onThemeChanged(result.data!.capabilities.theming!);
@ -162,411 +159,448 @@ class _HomePageState extends State<HomePage> {
final appsLoading,
final _,
) =>
RxBlocBuilder<AppsBloc, String?>(
StandardRxResultBuilder<AppsBloc, NotificationsApp?>(
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<AccountsBloc, List<Account>>(
bloc: accountsBloc,
state: (final bloc) => bloc.accounts,
RxBlocBuilder<AppsBloc, String?>(
bloc: _appsBloc,
state: (final bloc) => bloc.activeAppID,
builder: (
final context,
final accountsSnapshot,
final activeAppIDSnapshot,
final _,
) =>
OptionBuilder<NavigationMode>(
option: _globalOptions.navigationMode,
builder: (final context, final navigationMode) => WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context);
return true;
}
RxBlocBuilder<AccountsBloc, List<Account>>(
bloc: accountsBloc,
state: (final bloc) => bloc.accounts,
builder: (
final context,
final accountsSnapshot,
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();
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<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(
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<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);
},
),
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;
}
}
},
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<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,
);
}
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<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),
),
),
);
},
);
},
),
],
],
),
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();
},
),
),
),
),

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