Browse Source

RFC: errorBloc

pull/390/head
Nikolas Rimikis 2 years ago
parent
commit
6a6898de75
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 42
      packages/neon/neon/lib/src/blocs/apps.dart
  2. 2
      packages/neon/neon/lib/src/blocs/blocs.dart
  3. 88
      packages/neon/neon/lib/src/blocs/error.dart
  4. 321
      packages/neon/neon/lib/src/pages/home.dart

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

@ -50,6 +50,8 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
}
}),
);
unawaited(_checkCompatibility());
}
});
@ -59,11 +61,51 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
(final data) => data.capabilities.notifications != null ? _findAppImplementation('notifications') : null,
),
);
unawaited(_checkCompatibility());
});
unawaited(refresh());
}
Future<void> _checkCompatibility() async {
final apps = appImplementations.valueOrNull;
final capabilities = _capabilitiesBloc.capabilities.valueOrNull;
if (capabilities == null || apps == null) {
return;
}
final appIds = {
'core',
...apps.data!.map((final a) => a.id),
};
final notSupported = <(String, Object?)>{};
for (final id in appIds) {
try {
final (supported, minVersion) = switch (id) {
'core' => await _account.client.core.isSupported(capabilities.data),
'news' => await _account.client.news.isSupported(),
'notes' => await _account.client.notes.isSupported(capabilities.data),
_ => (true, null),
};
if (!supported) {
notSupported.add((id, minVersion));
}
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
}
}
if (notSupported.isNotEmpty) {
ErrorBloc().addVersionErrors(notSupported);
}
}
T? _findAppImplementation<T extends AppImplementation>(final String id) {
final matches = _filteredAppImplementations([id]);
if (matches.isNotEmpty) {

2
packages/neon/neon/lib/src/blocs/blocs.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/neon.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -14,6 +15,7 @@ import 'package:window_manager/window_manager.dart';
part 'accounts.dart';
part 'apps.dart';
part 'capabilities.dart';
part 'error.dart';
part 'first_launch.dart';
part 'login.dart';
part 'next_push.dart';

88
packages/neon/neon/lib/src/blocs/error.dart

@ -0,0 +1,88 @@
part of 'blocs.dart';
typedef TranslationCallback = String Function(AppLocalizations l10n);
abstract class ErrorBlocEvents {
/// Adds an error to the [ErrorBlocStates.globalErrors].
///
/// Used to signal non app specific errors.
void addGlobalError(final String message);
/// Adds an error to the [ErrorBlocStates.appErrors].
///
/// Used to signal errors specific to an app identified by [appId].
void addAppError(final String appId, final String message);
}
abstract class ErrorBlocStates {
/// Errors for the global neon framework.
BehaviorSubject<String> get globalErrors;
/// Errors for a specific app.
Map<String, BehaviorSubject<String>> get appErrors;
}
/// Holds error messages to be displayed by the UI
///
/// It will cache the last emmited error.
/// The [ErrorBloc] is a singleton.
class ErrorBloc extends Bloc implements ErrorBlocEvents, ErrorBlocStates {
factory ErrorBloc() => instance ??= ErrorBloc._();
@visibleForTesting
factory ErrorBloc.mocked(final ErrorBloc mock) => instance ??= mock;
ErrorBloc._();
@visibleForTesting
static ErrorBloc? instance;
AppLocalizations? l10n;
@override
final BehaviorSubject<String> globalErrors = BehaviorSubject();
@override
final Map<String, BehaviorSubject<String>> appErrors = {};
@override
void dispose() {
ErrorBloc.instance = null;
}
@override
void addGlobalError(final String message) {
globalErrors.add(message);
}
@override
void addAppError(final String appId, final String message) {
if (appErrors[appId] == null) {
appErrors[appId] = BehaviorSubject();
}
appErrors[appId]!.add(message);
}
void addVersionErrors(final Iterable<(String, Object?)> errors) {
assert(l10n != null, 'Localization must be register to process version Errors.');
final buffer = StringBuffer();
for (final error in errors) {
// TODO: reword errorUnsupportedVersion to support multiple errors
// TODO: add version info
final (appId, minVersion) = error;
final appName = l10n!.appImplementationName(appId);
final message = l10n!.errorUnsupportedVersion(appName);
buffer.write(message);
}
if (buffer.isNotEmpty) {
addGlobalError(buffer.toString());
}
}
String translateError(final TranslationCallback callback) => callback(l10n!);
}

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

@ -11,7 +11,6 @@ class HomePage extends StatefulWidget {
State<HomePage> createState() => _HomePageState();
}
// ignore: prefer_mixin
class _HomePageState extends State<HomePage> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final drawerScrollController = ScrollController();
@ -20,7 +19,6 @@ class _HomePageState extends State<HomePage> {
late GlobalOptions _globalOptions;
late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc;
late CapabilitiesBloc _capabilitiesBloc;
@override
void initState() {
@ -29,7 +27,6 @@ class _HomePageState extends State<HomePage> {
_accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
_account = _accountsBloc.activeAccount.value!;
_appsBloc = _accountsBloc.activeAppsBloc;
_capabilitiesBloc = _accountsBloc.activeCapabilitiesBloc;
_appsBloc.openNotifications.listen((final _) async {
final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull;
@ -42,47 +39,6 @@ class _HomePageState extends State<HomePage> {
}
});
_capabilitiesBloc.capabilities.listen((final result) async {
if (result.data != null) {
// ignore cached version and prevent duplicate dialogs
if (result.cached) {
return;
}
_appsBloc.appImplementations.listen((final appsResult) async {
// ignore cached version and prevent duplicate dialogs
if (appsResult.data == null || appsResult.cached) {
return;
}
for (final id in [
'core',
...appsResult.data!.map((final a) => a.id),
]) {
try {
final (supported, _) = switch (id) {
'core' => await _account.client.core.isSupported(result.data),
'news' => await _account.client.news.isSupported(),
'notes' => await _account.client.notes.isSupported(result.data),
_ => (true, null),
};
if (supported || !mounted) {
return;
}
var name = AppLocalizations.of(context).appImplementationName(id);
if (name == '') {
name = id;
}
await _showProblem(
AppLocalizations.of(context).errorUnsupportedVersion(name),
);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
}
}
});
}
});
GlobalPopups().register(context);
unawaited(_checkMaintenanceMode());
@ -168,164 +124,161 @@ class _HomePageState extends State<HomePage> {
}
@override
Widget build(final BuildContext context) => ResultBuilder<Capabilities>(
stream: _capabilitiesBloc.capabilities,
builder: (final context, final capabilities) => ResultBuilder<Iterable<AppImplementation>>(
stream: _appsBloc.appImplementations,
builder: (final context, final appImplementations) => ResultBuilder<NotificationsAppInterface?>(
stream: _appsBloc.notificationsAppImplementation,
builder: (final context, final notificationsAppImplementation) => StreamBuilder<String?>(
stream: _appsBloc.activeAppID,
builder: (final context, final activeAppIDSnapshot) => StreamBuilder<List<Account>>(
stream: _accountsBloc.accounts,
builder: (final context, final accountsSnapshot) => OptionBuilder<NavigationMode>(
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();
}
Widget build(final BuildContext context) => ResultBuilder<Iterable<AppImplementation>>(
stream: _appsBloc.appImplementations,
builder: (final context, final appImplementations) => ResultBuilder<NotificationsAppInterface?>(
stream: _appsBloc.notificationsAppImplementation,
builder: (final context, final notificationsAppImplementation) => StreamBuilder<String?>(
stream: _appsBloc.activeAppID,
builder: (final context, final activeAppIDSnapshot) => StreamBuilder<List<Account>>(
stream: _accountsBloc.accounts,
builder: (final context, final accountsSnapshot) => OptionBuilder<NavigationMode>(
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;
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.error != null) ...[
const SizedBox(
width: 8,
),
NeonException(
appImplementations.error,
onRetry: _appsBloc.refresh,
onlyIcon: true,
),
],
if (appImplementations.loading) ...[
const SizedBox(
width: 8,
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),
),
Expanded(
child: NeonLinearProgressIndicator(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
if (appImplementations.error != null) ...[
const SizedBox(
width: 8,
),
NeonException(
appImplementations.error,
onRetry: _appsBloc.refresh,
onlyIcon: true,
),
],
if (appImplementations.loading) ...[
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: [
if (notificationsAppImplementation.data != null) ...[
StreamBuilder<int>(
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),
),
);
},
),
if (accounts.length > 1) ...[
Text(
account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall,
),
],
IconButton(
onPressed: () {
AccountSettingsRoute(accountid: account.id).go(context);
],
),
actions: [
if (notificationsAppImplementation.data != null) ...[
StreamBuilder<int>(
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),
),
);
},
tooltip: AppLocalizations.of(context).settingsAccount,
icon: NeonUserAvatar(
account: account,
),
),
],
);
IconButton(
onPressed: () {
AccountSettingsRoute(accountid: account.id).go(context);
},
tooltip: AppLocalizations.of(context).settingsAccount,
icon: NeonUserAvatar(
account: account,
),
),
],
);
Widget body = Builder(
builder: (final context) {
if (appImplementations.data == null) {
return const SizedBox.shrink();
}
Widget body = Builder(
builder: (final context) {
if (appImplementations.data == null) {
return const SizedBox.shrink();
}
if (appImplementations.data!.isEmpty) {
return const NoAppsPage();
}
if (appImplementations.data!.isEmpty) {
return const NoAppsPage();
}
return appImplementations.data!.find(activeAppIDSnapshot.data!)!.page;
},
);
return 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,
),
);
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,
Expanded(
child: body,
),
],
);
}
if (drawerAlwaysVisible) {
body = Row(
children: [
drawer,
Expanded(
child: body,
),
],
);
}
return WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context);
return true;
}
return WillPopScope(
onWillPop: () async {
if (_scaffoldKey.currentState!.isDrawerOpen) {
Navigator.pop(context);
return true;
}
_scaffoldKey.currentState!.openDrawer();
return false;
},
child: body,
);
},
),
_scaffoldKey.currentState!.openDrawer();
return false;
},
child: body,
);
},
),
),
),

Loading…
Cancel
Save