Browse Source
This alligns the code with our documentations. Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>pull/1037/head
69 changed files with 646 additions and 641 deletions
@ -1,5 +1,5 @@
|
||||
export 'package:neon/src/bloc/bloc.dart'; |
||||
export 'package:neon/src/bloc/result.dart'; |
||||
// TODO: Remove access to the AccountsBloc. Apps should not need to access this |
||||
// TODO: Remove access to the AccountsBloc. Clients should not need to access this |
||||
export 'package:neon/src/blocs/accounts.dart' show AccountsBloc; |
||||
export 'package:neon/src/blocs/timer.dart' hide TimerBlocEvents, TimerBlocStates; |
||||
|
@ -1,3 +1,3 @@
|
||||
export 'package:neon/src/models/account.dart' hide Credentials, LoginQRcode; |
||||
export 'package:neon/src/models/app_implementation.dart'; |
||||
export 'package:neon/src/models/client_implementation.dart'; |
||||
export 'package:neon/src/models/notifications_interface.dart'; |
||||
|
@ -1,232 +0,0 @@
|
||||
import 'dart:async'; |
||||
|
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:neon/src/bloc/bloc.dart'; |
||||
import 'package:neon/src/bloc/result.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/app_implementation.dart'; |
||||
import 'package:neon/src/models/notifications_interface.dart'; |
||||
import 'package:neon/src/settings/models/options_collection.dart'; |
||||
import 'package:neon/src/utils/request_manager.dart'; |
||||
import 'package:nextcloud/core.dart' as core; |
||||
import 'package:nextcloud/nextcloud.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:rxdart/rxdart.dart'; |
||||
|
||||
@internal |
||||
abstract interface class AppsBlocEvents { |
||||
/// Sets the active app using the [appID]. |
||||
/// |
||||
/// If the app is already the active app nothing will happen. |
||||
/// When using [skipAlreadySet] nothing will be done if there already is an active app. |
||||
void setActiveApp(final String appID, {final bool skipAlreadySet = false}); |
||||
} |
||||
|
||||
@internal |
||||
abstract interface class AppsBlocStates { |
||||
BehaviorSubject<Result<Iterable<AppImplementation>>> get appImplementations; |
||||
|
||||
BehaviorSubject<Result<NotificationsAppInterface?>> get notificationsAppImplementation; |
||||
|
||||
BehaviorSubject<AppImplementation> get activeApp; |
||||
|
||||
BehaviorSubject<void> get openNotifications; |
||||
|
||||
BehaviorSubject<Map<String, String?>> get appVersions; |
||||
} |
||||
|
||||
@internal |
||||
class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates { |
||||
AppsBloc( |
||||
this._capabilitiesBloc, |
||||
this._accountsBloc, |
||||
this._account, |
||||
this._allAppImplementations, |
||||
) { |
||||
_apps.listen((final result) { |
||||
appImplementations |
||||
.add(result.transform((final data) => _filteredAppImplementations(data.map((final a) => a.id)))); |
||||
}); |
||||
|
||||
appImplementations.listen((final result) async { |
||||
if (!result.hasData) { |
||||
return; |
||||
} |
||||
|
||||
// dispose unsupported apps |
||||
for (final app in _allAppImplementations) { |
||||
if (result.requireData.tryFind(app.id) == null) { |
||||
app.blocsCache.remove(_account); |
||||
} |
||||
} |
||||
|
||||
final options = _accountsBloc.getOptionsFor(_account); |
||||
final initialApp = options.initialApp.value ?? _getInitialAppFallback(); |
||||
if (initialApp != null) { |
||||
await setActiveApp(initialApp, skipAlreadySet: true); |
||||
} |
||||
|
||||
unawaited(_checkCompatibility()); |
||||
}); |
||||
|
||||
_capabilitiesBloc.capabilities.listen((final result) { |
||||
notificationsAppImplementation.add( |
||||
result.transform( |
||||
(final data) => data.capabilities.notificationsCapabilities?.notifications != null |
||||
? _findAppImplementation(AppIDs.notifications) |
||||
: null, |
||||
), |
||||
); |
||||
|
||||
unawaited(_checkCompatibility()); |
||||
}); |
||||
|
||||
unawaited(refresh()); |
||||
} |
||||
|
||||
/// Determines the appid of initial app. |
||||
/// |
||||
/// It requires [appImplementations] to have both a value and data. |
||||
/// |
||||
/// The files app is always installed and can not be removed so it will be used, but in the |
||||
/// case this changes at a later point the first supported app will be returned. |
||||
/// |
||||
/// Returns null when no app is supported by the server. |
||||
String? _getInitialAppFallback() { |
||||
final supportedApps = appImplementations.value.requireData; |
||||
|
||||
for (final fallback in {AppIDs.dashboard, AppIDs.files}) { |
||||
if (supportedApps.tryFind(fallback) != null) { |
||||
return fallback; |
||||
} |
||||
} |
||||
|
||||
if (supportedApps.isNotEmpty) { |
||||
return supportedApps.first.id; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
Future<void> _checkCompatibility() async { |
||||
final apps = appImplementations.valueOrNull; |
||||
final capabilities = _capabilitiesBloc.capabilities.valueOrNull; |
||||
|
||||
// ignore cached data |
||||
if (capabilities == null || apps == null || !capabilities.hasUncachedData || !apps.hasUncachedData) { |
||||
return; |
||||
} |
||||
|
||||
final notSupported = <String, String?>{}; |
||||
|
||||
try { |
||||
final (coreSupported, coreMinimumVersion) = _account.client.core.isSupported(capabilities.requireData); |
||||
if (!coreSupported) { |
||||
notSupported['core'] = coreMinimumVersion.toString(); |
||||
} |
||||
} catch (e, s) { |
||||
debugPrint(e.toString()); |
||||
debugPrint(s.toString()); |
||||
} |
||||
|
||||
for (final app in apps.requireData) { |
||||
try { |
||||
final (supported, minimumVersion) = await app.isSupported(_account, capabilities.requireData); |
||||
if (!(supported ?? true)) { |
||||
notSupported[app.id] = minimumVersion; |
||||
} |
||||
} catch (e, s) { |
||||
debugPrint(e.toString()); |
||||
debugPrint(s.toString()); |
||||
} |
||||
} |
||||
|
||||
if (notSupported.isNotEmpty) { |
||||
appVersions.add(notSupported); |
||||
} |
||||
} |
||||
|
||||
T? _findAppImplementation<T extends AppImplementation>(final String id) { |
||||
final matches = _filteredAppImplementations([id]); |
||||
if (matches.isNotEmpty) { |
||||
return matches.single as T; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
Iterable<AppImplementation> _filteredAppImplementations(final Iterable<String> appIds) => |
||||
_allAppImplementations.where((final a) => appIds.contains(a.id)); |
||||
|
||||
final CapabilitiesBloc _capabilitiesBloc; |
||||
final AccountsBloc _accountsBloc; |
||||
final Account _account; |
||||
final Iterable<AppImplementation> _allAppImplementations; |
||||
final _apps = BehaviorSubject<Result<List<core.NavigationEntry>>>(); |
||||
|
||||
@override |
||||
void dispose() { |
||||
unawaited(_apps.close()); |
||||
unawaited(appImplementations.close()); |
||||
unawaited(notificationsAppImplementation.close()); |
||||
unawaited(activeApp.close()); |
||||
unawaited(openNotifications.close()); |
||||
unawaited(appVersions.close()); |
||||
|
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
BehaviorSubject<AppImplementation> activeApp = BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<Result<Iterable<AppImplementation<Bloc, NextcloudAppOptions>>>> appImplementations = |
||||
BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<Result<NotificationsAppInterface?>> notificationsAppImplementation = BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<void> openNotifications = BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<Map<String, String?>> appVersions = BehaviorSubject(); |
||||
|
||||
@override |
||||
Future<void> refresh() async { |
||||
await RequestManager.instance.wrapNextcloud( |
||||
_account.id, |
||||
'apps-apps', |
||||
_apps, |
||||
_account.client.core.navigation.getAppsNavigationRaw(), |
||||
(final response) => response.body.ocs.data.toList(), |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Future<void> setActiveApp(final String appID, {final bool skipAlreadySet = false}) async { |
||||
if (appID == AppIDs.notifications) { |
||||
openNotifications.add(null); |
||||
return; |
||||
} |
||||
|
||||
final apps = await appImplementations.firstWhere((final a) => a.hasData); |
||||
final app = apps.requireData.tryFind(appID); |
||||
if (app != null) { |
||||
if ((!activeApp.hasValue || !skipAlreadySet) && activeApp.valueOrNull?.id != appID) { |
||||
activeApp.add(app); |
||||
} |
||||
} else { |
||||
throw Exception('App $appID not found'); |
||||
} |
||||
} |
||||
|
||||
T getAppBloc<T extends Bloc>(final AppImplementation<T, dynamic> appImplementation) => |
||||
appImplementation.getBloc(_account); |
||||
|
||||
List<Provider<Bloc>> get appBlocProviders => |
||||
_allAppImplementations.map((final appImplementation) => appImplementation.blocProvider).toList(); |
||||
} |
@ -0,0 +1,232 @@
|
||||
import 'dart:async'; |
||||
|
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:neon/src/bloc/bloc.dart'; |
||||
import 'package:neon/src/bloc/result.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/client_implementation.dart'; |
||||
import 'package:neon/src/models/notifications_interface.dart'; |
||||
import 'package:neon/src/settings/models/options_collection.dart'; |
||||
import 'package:neon/src/utils/request_manager.dart'; |
||||
import 'package:nextcloud/core.dart' as core; |
||||
import 'package:nextcloud/nextcloud.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:rxdart/rxdart.dart'; |
||||
|
||||
@internal |
||||
abstract interface class ClientsBlocEvents { |
||||
/// Sets the active client using the [clientID]. |
||||
/// |
||||
/// If the client is already the active client nothing will happen. |
||||
/// When using [skipAlreadySet] nothing will be done if there already is an active client. |
||||
void setActiveClient(final String clientID, {final bool skipAlreadySet = false}); |
||||
} |
||||
|
||||
@internal |
||||
abstract interface class ClientsBlocStates { |
||||
BehaviorSubject<Result<Iterable<ClientImplementation>>> get clientImplementations; |
||||
|
||||
BehaviorSubject<Result<NotificationsClientInterface?>> get notificationsClientImplementation; |
||||
|
||||
BehaviorSubject<ClientImplementation> get activeClient; |
||||
|
||||
BehaviorSubject<void> get openNotifications; |
||||
|
||||
BehaviorSubject<Map<String, String?>> get clientVersions; |
||||
} |
||||
|
||||
@internal |
||||
class ClientsBloc extends InteractiveBloc implements ClientsBlocEvents, ClientsBlocStates { |
||||
ClientsBloc( |
||||
this._capabilitiesBloc, |
||||
this._accountsBloc, |
||||
this._account, |
||||
this._allClientImplementations, |
||||
) { |
||||
_clients.listen((final result) { |
||||
clientImplementations |
||||
.add(result.transform((final data) => _filteredClientImplementations(data.map((final c) => c.id)))); |
||||
}); |
||||
|
||||
clientImplementations.listen((final result) async { |
||||
if (!result.hasData) { |
||||
return; |
||||
} |
||||
|
||||
// dispose unsupported clients |
||||
for (final client in _allClientImplementations) { |
||||
if (result.requireData.tryFind(client.id) == null) { |
||||
client.blocsCache.remove(_account); |
||||
} |
||||
} |
||||
|
||||
final options = _accountsBloc.getOptionsFor(_account); |
||||
final initialClient = options.initialClient.value ?? _getInitialClientFallback(); |
||||
if (initialClient != null) { |
||||
await setActiveClient(initialClient, skipAlreadySet: true); |
||||
} |
||||
|
||||
unawaited(_checkCompatibility()); |
||||
}); |
||||
|
||||
_capabilitiesBloc.capabilities.listen((final result) { |
||||
notificationsClientImplementation.add( |
||||
result.transform( |
||||
(final data) => data.capabilities.notificationsCapabilities?.notifications != null |
||||
? _findClientImplementation(AppIDs.notifications) |
||||
: null, |
||||
), |
||||
); |
||||
|
||||
unawaited(_checkCompatibility()); |
||||
}); |
||||
|
||||
unawaited(refresh()); |
||||
} |
||||
|
||||
/// Determines the clientid of initial client. |
||||
/// |
||||
/// It requires [clientImplementations] to have both a value and data. |
||||
/// |
||||
/// The files client is always installed and can not be removed so it will be used, but in the |
||||
/// case this changes at a later point the first supported client will be returned. |
||||
/// |
||||
/// Returns null when no client is supported by the server. |
||||
String? _getInitialClientFallback() { |
||||
final supportedClients = clientImplementations.value.requireData; |
||||
|
||||
for (final fallback in {AppIDs.dashboard, AppIDs.files}) { |
||||
if (supportedClients.tryFind(fallback) != null) { |
||||
return fallback; |
||||
} |
||||
} |
||||
|
||||
if (supportedClients.isNotEmpty) { |
||||
return supportedClients.first.id; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
Future<void> _checkCompatibility() async { |
||||
final clients = clientImplementations.valueOrNull; |
||||
final capabilities = _capabilitiesBloc.capabilities.valueOrNull; |
||||
|
||||
// ignore cached data |
||||
if (capabilities == null || clients == null || !capabilities.hasUncachedData || !clients.hasUncachedData) { |
||||
return; |
||||
} |
||||
|
||||
final notSupported = <String, String?>{}; |
||||
|
||||
try { |
||||
final (coreSupported, coreMinimumVersion) = _account.client.core.isSupported(capabilities.requireData); |
||||
if (!coreSupported) { |
||||
notSupported['core'] = coreMinimumVersion.toString(); |
||||
} |
||||
} catch (e, s) { |
||||
debugPrint(e.toString()); |
||||
debugPrint(s.toString()); |
||||
} |
||||
|
||||
for (final client in clients.requireData) { |
||||
try { |
||||
final (supported, minimumVersion) = await client.isSupported(_account, capabilities.requireData); |
||||
if (!(supported ?? true)) { |
||||
notSupported[client.id] = minimumVersion; |
||||
} |
||||
} catch (e, s) { |
||||
debugPrint(e.toString()); |
||||
debugPrint(s.toString()); |
||||
} |
||||
} |
||||
|
||||
if (notSupported.isNotEmpty) { |
||||
clientVersions.add(notSupported); |
||||
} |
||||
} |
||||
|
||||
T? _findClientImplementation<T extends ClientImplementation>(final String id) { |
||||
final matches = _filteredClientImplementations([id]); |
||||
if (matches.isNotEmpty) { |
||||
return matches.single as T; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
Iterable<ClientImplementation> _filteredClientImplementations(final Iterable<String> clientIds) => |
||||
_allClientImplementations.where((final c) => clientIds.contains(c.id)); |
||||
|
||||
final CapabilitiesBloc _capabilitiesBloc; |
||||
final AccountsBloc _accountsBloc; |
||||
final Account _account; |
||||
final Iterable<ClientImplementation> _allClientImplementations; |
||||
final _clients = BehaviorSubject<Result<List<core.NavigationEntry>>>(); |
||||
|
||||
@override |
||||
void dispose() { |
||||
unawaited(_clients.close()); |
||||
unawaited(clientImplementations.close()); |
||||
unawaited(notificationsClientImplementation.close()); |
||||
unawaited(activeClient.close()); |
||||
unawaited(openNotifications.close()); |
||||
unawaited(clientVersions.close()); |
||||
|
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
BehaviorSubject<ClientImplementation> activeClient = BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<Result<Iterable<ClientImplementation<Bloc, NextcloudClientOptions>>>> clientImplementations = |
||||
BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<Result<NotificationsClientInterface?>> notificationsClientImplementation = BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<void> openNotifications = BehaviorSubject(); |
||||
|
||||
@override |
||||
BehaviorSubject<Map<String, String?>> clientVersions = BehaviorSubject(); |
||||
|
||||
@override |
||||
Future<void> refresh() async { |
||||
await RequestManager.instance.wrapNextcloud( |
||||
_account.id, |
||||
'apps-apps', |
||||
_clients, |
||||
_account.client.core.navigation.getAppsNavigationRaw(), |
||||
(final response) => response.body.ocs.data.toList(), |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Future<void> setActiveClient(final String clientID, {final bool skipAlreadySet = false}) async { |
||||
if (clientID == AppIDs.notifications) { |
||||
openNotifications.add(null); |
||||
return; |
||||
} |
||||
|
||||
final clients = await clientImplementations.firstWhere((final c) => c.hasData); |
||||
final client = clients.requireData.tryFind(clientID); |
||||
if (client != null) { |
||||
if ((!activeClient.hasValue || !skipAlreadySet) && activeClient.valueOrNull?.id != clientID) { |
||||
activeClient.add(client); |
||||
} |
||||
} else { |
||||
throw Exception('Client $clientID not found'); |
||||
} |
||||
} |
||||
|
||||
T getClientBloc<T extends Bloc>(final ClientImplementation<T, dynamic> clientImplementation) => |
||||
clientImplementation.getBloc(_account); |
||||
|
||||
List<Provider<Bloc>> get clientBlocProviders => |
||||
_allClientImplementations.map((final clientImplementation) => clientImplementation.blocProvider).toList(); |
||||
} |
@ -1,30 +0,0 @@
|
||||
import 'package:mocktail/mocktail.dart'; |
||||
import 'package:neon/src/models/app_implementation.dart'; |
||||
import 'package:test/test.dart'; |
||||
|
||||
// ignore: missing_override_of_must_be_overridden, avoid_implementing_value_types |
||||
class AppImplementationMock extends Mock implements AppImplementation {} |
||||
|
||||
void main() { |
||||
group('group name', () { |
||||
test('AccountFind', () { |
||||
final app1 = AppImplementationMock(); |
||||
final app2 = AppImplementationMock(); |
||||
|
||||
final apps = { |
||||
app1, |
||||
app2, |
||||
}; |
||||
|
||||
when(() => app1.id).thenReturn('app1'); |
||||
when(() => app2.id).thenReturn('app2'); |
||||
|
||||
expect(apps.tryFind(null), isNull); |
||||
expect(apps.tryFind('invalidID'), isNull); |
||||
expect(apps.tryFind(app2.id), equals(app2)); |
||||
|
||||
expect(() => apps.find('invalidID'), throwsA(isA<StateError>())); |
||||
expect(apps.find(app2.id), equals(app2)); |
||||
}); |
||||
}); |
||||
} |
@ -0,0 +1,30 @@
|
||||
import 'package:mocktail/mocktail.dart'; |
||||
import 'package:neon/src/models/client_implementation.dart'; |
||||
import 'package:test/test.dart'; |
||||
|
||||
// ignore: missing_override_of_must_be_overridden, avoid_implementing_value_types |
||||
class ClientImplementationMock extends Mock implements ClientImplementation {} |
||||
|
||||
void main() { |
||||
group('group name', () { |
||||
test('AccountFind', () { |
||||
final client1 = ClientImplementationMock(); |
||||
final client2 = ClientImplementationMock(); |
||||
|
||||
final apps = { |
||||
client1, |
||||
client2, |
||||
}; |
||||
|
||||
when(() => client1.id).thenReturn('app1'); |
||||
when(() => client2.id).thenReturn('app2'); |
||||
|
||||
expect(apps.tryFind(null), isNull); |
||||
expect(apps.tryFind('invalidID'), isNull); |
||||
expect(apps.tryFind(client2.id), equals(client2)); |
||||
|
||||
expect(() => apps.find('invalidID'), throwsA(isA<StateError>())); |
||||
expect(apps.find(client2.id), equals(client2)); |
||||
}); |
||||
}); |
||||
} |
Loading…
Reference in new issue