Browse Source

refactor: rename app to client

This alligns the code with our documentations.
Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/1037/head
Nikolas Rimikis 2 years ago
parent
commit
43382e221c
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 1
      .cspell/neon.txt
  2. 2
      packages/app/integration_test/screenshot_test.dart
  3. 12
      packages/app/lib/apps.dart
  4. 2
      packages/app/lib/main.dart
  5. 2
      packages/neon/neon/lib/blocs.dart
  6. 4
      packages/neon/neon/lib/l10n/en.arb
  7. 4
      packages/neon/neon/lib/l10n/localizations.dart
  8. 2
      packages/neon/neon/lib/l10n/localizations_en.dart
  9. 2
      packages/neon/neon/lib/models.dart
  10. 12
      packages/neon/neon/lib/neon.dart
  11. 58
      packages/neon/neon/lib/src/app.dart
  12. 42
      packages/neon/neon/lib/src/blocs/accounts.dart
  13. 232
      packages/neon/neon/lib/src/blocs/apps.dart
  14. 232
      packages/neon/neon/lib/src/blocs/clients.dart
  15. 2
      packages/neon/neon/lib/src/blocs/push_notifications.dart
  16. 18
      packages/neon/neon/lib/src/blocs/unified_search.dart
  17. 26
      packages/neon/neon/lib/src/models/client_implementation.dart
  18. 10
      packages/neon/neon/lib/src/models/notifications_interface.dart
  19. 2
      packages/neon/neon/lib/src/pages/account_settings.dart
  20. 24
      packages/neon/neon/lib/src/pages/client_settings.dart
  21. 42
      packages/neon/neon/lib/src/pages/home.dart
  22. 26
      packages/neon/neon/lib/src/pages/settings.dart
  23. 24
      packages/neon/neon/lib/src/router.dart
  24. 16
      packages/neon/neon/lib/src/router.g.dart
  25. 8
      packages/neon/neon/lib/src/settings/models/options_collection.dart
  26. 6
      packages/neon/neon/lib/src/settings/models/storage.dart
  27. 22
      packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart
  28. 6
      packages/neon/neon/lib/src/theme/theme.dart
  29. 20
      packages/neon/neon/lib/src/utils/account_options.dart
  30. 14
      packages/neon/neon/lib/src/utils/client_route.dart
  31. 2
      packages/neon/neon/lib/src/utils/global_options.dart
  32. 20
      packages/neon/neon/lib/src/utils/push_utils.dart
  33. 66
      packages/neon/neon/lib/src/widgets/app_bar.dart
  34. 12
      packages/neon/neon/lib/src/widgets/client_implementation_icon.dart
  35. 48
      packages/neon/neon/lib/src/widgets/drawer.dart
  36. 2
      packages/neon/neon/lib/utils.dart
  37. 30
      packages/neon/neon/test/app_implementation_test.dart
  38. 30
      packages/neon/neon/test/client_implementation_test.dart
  39. 4
      packages/neon/neon/test/options_collection_test.dart
  40. 10
      packages/neon/neon/test/settings_export_test.dart
  41. 6
      packages/neon/neon/test/storage_test.dart
  42. 10
      packages/neon/neon_dashboard/lib/src/app.dart
  43. 6
      packages/neon/neon_dashboard/lib/src/options.dart
  44. 12
      packages/neon/neon_dashboard/lib/src/routes.dart
  45. 10
      packages/neon/neon_dashboard/lib/src/routes.g.dart
  46. 2
      packages/neon/neon_files/lib/blocs/browser.dart
  47. 2
      packages/neon/neon_files/lib/blocs/files.dart
  48. 8
      packages/neon/neon_files/lib/neon_files.dart
  49. 4
      packages/neon/neon_files/lib/options.dart
  50. 8
      packages/neon/neon_files/lib/routes.dart
  51. 10
      packages/neon/neon_files/lib/routes.g.dart
  52. 2
      packages/neon/neon_news/lib/blocs/articles.dart
  53. 2
      packages/neon/neon_news/lib/blocs/news.dart
  54. 8
      packages/neon/neon_news/lib/neon_news.dart
  55. 4
      packages/neon/neon_news/lib/options.dart
  56. 8
      packages/neon/neon_news/lib/routes.dart
  57. 10
      packages/neon/neon_news/lib/routes.g.dart
  58. 2
      packages/neon/neon_notes/lib/blocs/note.dart
  59. 2
      packages/neon/neon_notes/lib/blocs/notes.dart
  60. 8
      packages/neon/neon_notes/lib/neon_notes.dart
  61. 4
      packages/neon/neon_notes/lib/options.dart
  62. 8
      packages/neon/neon_notes/lib/routes.dart
  63. 10
      packages/neon/neon_notes/lib/routes.g.dart
  64. 2
      packages/neon/neon_notifications/lib/blocs/notifications.dart
  65. 10
      packages/neon/neon_notifications/lib/neon_notifications.dart
  66. 4
      packages/neon/neon_notifications/lib/options.dart
  67. 10
      packages/neon/neon_notifications/lib/pages/main.dart
  68. 8
      packages/neon/neon_notifications/lib/routes.dart
  69. 10
      packages/neon/neon_notifications/lib/routes.g.dart

1
.cspell/neon.txt

@ -1,3 +1,4 @@
clientid
crypton
exportables
fcmup

2
packages/app/integration_test/screenshot_test.dart

@ -24,7 +24,7 @@ Future<void> runTestApp(
final Account? account,
}) async {
await runNeon(
appImplementations: appImplementations,
clientImplementations: clientImplementations,
theme: neonTheme,
bindingOverride: binding,
account: account,

12
packages/app/lib/apps.dart

@ -6,10 +6,10 @@ import 'package:neon_notes/neon_notes.dart';
import 'package:neon_notifications/neon_notifications.dart';
/// The collection of clients enabled for the Neon app.
final Set<AppImplementation> appImplementations = {
DashboardApp(),
FilesApp(),
NewsApp(),
NotesApp(),
NotificationsApp(),
final Set<ClientImplementation> clientImplementations = {
DashboardClient(),
FilesClient(),
NewsClient(),
NotesClient(),
NotificationsClient(),
};

2
packages/app/lib/main.dart

@ -4,7 +4,7 @@ import 'package:neon/neon.dart';
Future<void> main() async {
await runNeon(
appImplementations: appImplementations,
clientImplementations: clientImplementations,
theme: neonTheme,
);
}

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

@ -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;

4
packages/neon/neon/lib/l10n/en.arb

@ -2,8 +2,8 @@
"@@locale": "en",
"nextcloud": "Nextcloud",
"nextcloudLogo": "Nextcloud logo",
"appImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} dashboard{Dashboard} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}",
"@appImplementationName": {
"clientImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} dashboard{Dashboard} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}",
"@clientImplementationName": {
"placeholders": {
"app": {}
}

4
packages/neon/neon/lib/l10n/localizations.dart

@ -101,11 +101,11 @@ abstract class NeonLocalizations {
/// **'Nextcloud logo'**
String get nextcloudLogo;
/// No description provided for @appImplementationName.
/// No description provided for @clientImplementationName.
///
/// In en, this message translates to:
/// **'{app, select, nextcloud{Nextcloud} core{Server} dashboard{Dashboard} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}'**
String appImplementationName(String app);
String clientImplementationName(String app);
/// No description provided for @loginAgain.
///

2
packages/neon/neon/lib/l10n/localizations_en.dart

@ -13,7 +13,7 @@ class NeonLocalizationsEn extends NeonLocalizations {
String get nextcloudLogo => 'Nextcloud logo';
@override
String appImplementationName(String app) {
String clientImplementationName(String app) {
String _temp0 = intl.Intl.selectLogic(
app,
{

2
packages/neon/neon/lib/models.dart

@ -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';

12
packages/neon/neon/lib/neon.dart

@ -8,7 +8,7 @@ import 'package:neon/src/blocs/first_launch.dart';
import 'package:neon/src/blocs/next_push.dart';
import 'package:neon/src/blocs/push_notifications.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/models/disposable.dart';
import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/settings/models/storage.dart';
@ -21,7 +21,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
Future<void> runNeon({
required final Set<AppImplementation> appImplementations,
required final Set<ClientImplementation> clientImplementations,
required final NeonTheme theme,
@visibleForTesting final WidgetsBinding? bindingOverride,
@visibleForTesting final Account? account,
@ -44,7 +44,7 @@ Future<void> runNeon({
final accountsBloc = AccountsBloc(
globalOptions,
appImplementations,
clientImplementations,
);
if (account != null) {
accountsBloc
@ -71,9 +71,9 @@ Future<void> runNeon({
NeonProvider<AccountsBloc>.value(value: accountsBloc),
NeonProvider<FirstLaunchBloc>.value(value: firstLaunchBloc),
NeonProvider<NextPushBloc>.value(value: nextPushBloc),
Provider<Iterable<AppImplementation>>(
create: (final _) => appImplementations,
dispose: (final _, final appImplementations) => appImplementations.disposeAll(),
Provider<Iterable<ClientImplementation>>(
create: (final _) => clientImplementations,
dispose: (final _, final clientImplementations) => clientImplementations.disposeAll(),
),
Provider<PackageInfo>.value(value: packageInfo),
],

58
packages/neon/neon/lib/src/app.dart

@ -10,7 +10,7 @@ import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/models/notifications_interface.dart';
import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/platform/platform.dart';
@ -46,7 +46,7 @@ class NeonApp extends StatefulWidget {
class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.TrayListener, WindowListener {
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true);
final _navigatorKey = GlobalKey<NavigatorState>();
late final Iterable<AppImplementation> _appImplementations;
late final Iterable<ClientImplementation> _clientImplementations;
late final GlobalOptions _globalOptions;
late final AccountsBloc _accountsBloc;
late final _routerDelegate = buildAppRouter(
@ -60,7 +60,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
void initState() {
super.initState();
_appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
_clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
_globalOptions = NeonProvider.of<GlobalOptions>(context);
_accountsBloc = NeonProvider.of<AccountsBloc>(context);
@ -81,12 +81,12 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
if (NeonPlatform.instance.canUseQuickActions) {
const quickActions = QuickActions();
await quickActions.setShortcutItems(
_appImplementations
_clientImplementations
.map(
(final app) => ShortcutItem(
type: 'app_${app.id}',
localizedTitle: app.nameFromLocalization(localizations),
icon: 'app_${app.id}',
(final client) => ShortcutItem(
type: 'app_${client.id}',
localizedTitle: client.nameFromLocalization(localizations),
icon: 'app_${client.id}',
),
)
.toList(),
@ -111,10 +111,10 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
await tray.trayManager.setContextMenu(
tray.Menu(
items: [
for (final app in _appImplementations) ...[
for (final client in _clientImplementations) ...[
tray.MenuItem(
key: 'app_${app.id}',
label: app.nameFromLocalization(localizations),
key: 'app_${client.id}',
label: client.nameFromLocalization(localizations),
// TODO: Add icons which should work on macOS and Windows
),
],
@ -145,14 +145,14 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
return;
}
final allAppImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final app = allAppImplementations.tryFind(AppIDs.notifications) as NotificationsAppInterface?;
final allClientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
final client = allClientImplementations.tryFind(AppIDs.notifications) as NotificationsClientInterface?;
if (app == null) {
if (client == null) {
return;
}
await _accountsBloc.getAppsBlocFor(account).getAppBloc<NotificationsBlocInterface>(app).refresh();
await _accountsBloc.getClientsBlocFor(account).getClientBloc<NotificationsBlocInterface>(client).refresh();
};
Global.onPushNotificationClicked = (final pushNotificationWithAccountID) async {
final account = _accountsBloc.accounts.value.tryFind(pushNotificationWithAccountID.accountID);
@ -161,22 +161,24 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
}
_accountsBloc.setActiveAccount(account);
final allAppImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final allClientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
final notificationsApp = allAppImplementations.tryFind(AppIDs.notifications) as NotificationsAppInterface?;
if (notificationsApp != null) {
final notificationsClient =
allClientImplementations.tryFind(AppIDs.notifications) as NotificationsClientInterface?;
if (notificationsClient != null) {
_accountsBloc
.getAppsBlocFor(account)
.getAppBloc<NotificationsBlocInterface>(notificationsApp)
.getClientsBlocFor(account)
.getClientBloc<NotificationsBlocInterface>(notificationsClient)
.deleteNotification(pushNotificationWithAccountID.subject.nid!);
}
final app = allAppImplementations.tryFind(pushNotificationWithAccountID.subject.app) ?? notificationsApp;
if (app == null) {
final client =
allClientImplementations.tryFind(pushNotificationWithAccountID.subject.app) ?? notificationsClient;
if (client == null) {
return;
}
await _openAppFromExternal(account, app.id);
await _openAppFromExternal(account, client.id);
};
final details = await localNotificationsPlugin.getNotificationAppLaunchDetails();
@ -235,7 +237,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
}
Future<void> _openAppFromExternal(final Account account, final String id) async {
await _accountsBloc.getAppsBlocFor(account).setActiveApp(id);
await _accountsBloc.getClientsBlocFor(account).setActiveClient(id);
_navigatorKey.currentState!.popUntil((final route) => route.settings.name == 'home');
await _showAndRestoreWindow();
}
@ -291,17 +293,19 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
capabilitiesSnapshot.data?.capabilities.themingPublicCapabilities?.theming,
keepOriginalAccentColor: options.themeKeepOriginalAccentColor.value,
oledAsDark: options.themeOLEDAsDark.value,
appThemes: _appImplementations.map((final a) => a.theme).whereNotNull(),
clientThemes: _clientImplementations.map((final c) => c.theme).whereNotNull(),
neonTheme: widget.neonTheme,
);
return MaterialApp.router(
localizationsDelegates: [
..._appImplementations.map((final app) => app.localizationsDelegate),
..._clientImplementations.map((final client) => client.localizationsDelegate),
...NeonLocalizations.localizationsDelegates,
],
supportedLocales: {
..._appImplementations.map((final app) => app.supportedLocales).expand((final element) => element),
..._clientImplementations
.map((final client) => client.supportedLocales)
.expand((final element) => element),
...NeonLocalizations.supportedLocales,
},
themeMode: options.themeMode.value,

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

@ -5,14 +5,14 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/bloc/bloc.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:neon/src/blocs/unified_search.dart';
import 'package:neon/src/blocs/user_details.dart';
import 'package:neon/src/blocs/user_statuses.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/account_cache.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/utils/account_options.dart';
import 'package:neon/src/utils/global_options.dart';
@ -62,7 +62,7 @@ abstract interface class AccountsBlocStates {
class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocStates {
AccountsBloc(
this._globalOptions,
this._allAppImplementations,
this._allClientImplementations,
) {
const lastUsedStorage = SingleValueStorage(StorageKeys.lastUsedAccount);
@ -103,22 +103,22 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
accounts.listen((final accounts) {
_accountsOptions.pruneAgainst(accounts);
_appsBlocs.pruneAgainst(accounts);
_clientsBlocs.pruneAgainst(accounts);
_capabilitiesBlocs.pruneAgainst(accounts);
_userDetailsBlocs.pruneAgainst(accounts);
_userStatusesBlocs.pruneAgainst(accounts);
_unifiedSearchBlocs.pruneAgainst(accounts);
for (final app in _allAppImplementations) {
app.blocsCache.pruneAgainst(accounts);
for (final client in _allClientImplementations) {
client.blocsCache.pruneAgainst(accounts);
}
});
}
final GlobalOptions _globalOptions;
final Iterable<AppImplementation> _allAppImplementations;
final Iterable<ClientImplementation> _allClientImplementations;
final _accountsOptions = AccountCache<AccountSpecificOptions>();
final _appsBlocs = AccountCache<AppsBloc>();
final _clientsBlocs = AccountCache<ClientsBloc>();
final _capabilitiesBlocs = AccountCache<CapabilitiesBloc>();
final _userDetailsBlocs = AccountCache<UserDetailsBloc>();
final _userStatusesBlocs = AccountCache<UserStatusesBloc>();
@ -128,7 +128,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
void dispose() {
unawaited(activeAccount.close());
unawaited(accounts.close());
_appsBlocs.dispose();
_clientsBlocs.dispose();
_capabilitiesBlocs.dispose();
_userDetailsBlocs.dispose();
_userStatusesBlocs.dispose();
@ -226,23 +226,23 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
///
/// Use [activeOptions] to get them for the [activeAccount].
AccountSpecificOptions getOptionsFor(final Account account) => _accountsOptions[account] ??= AccountSpecificOptions(
AppStorage(StorageKeys.accounts, account.id),
getAppsBlocFor(account),
ClientStorage(StorageKeys.accounts, account.id),
getClientsBlocFor(account),
);
/// The appsBloc for the [activeAccount].
/// The clientsBloc for the [activeAccount].
///
/// Convenience method for [getAppsBlocFor] with the currently active account.
AppsBloc get activeAppsBloc => getAppsBlocFor(aa);
/// Convenience method for [getClientsBlocFor] with the currently active account.
ClientsBloc get activeClientsBloc => getClientsBlocFor(aa);
/// The appsBloc for the specified [account].
/// The clientsBloc for the specified [account].
///
/// Use [activeAppsBloc] to get them for the [activeAccount].
AppsBloc getAppsBlocFor(final Account account) => _appsBlocs[account] ??= AppsBloc(
/// Use [activeClientsBloc] to get them for the [activeAccount].
ClientsBloc getClientsBlocFor(final Account account) => _clientsBlocs[account] ??= ClientsBloc(
getCapabilitiesBlocFor(account),
this,
account,
_allAppImplementations,
_allClientImplementations,
);
/// The capabilitiesBloc for the [activeAccount].
@ -288,7 +288,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// Use [activeUnifiedSearchBloc] to get them for the [activeAccount].
UnifiedSearchBloc getUnifiedSearchBlocFor(final Account account) =>
_unifiedSearchBlocs[account] ??= UnifiedSearchBloc(
getAppsBlocFor(account),
getClientsBlocFor(account),
account,
);
}
@ -297,7 +297,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
///
/// It is not checked whether the stored information is still valid.
List<Account> loadAccounts() {
const storage = AppStorage(StorageKeys.accounts);
const storage = ClientStorage(StorageKeys.accounts);
if (storage.containsKey(_keyAccounts)) {
return storage
@ -310,7 +310,7 @@ List<Account> loadAccounts() {
/// Saves the given [accounts] to the storage.
Future<void> saveAccounts(final List<Account> accounts) async {
const storage = AppStorage(StorageKeys.accounts);
const storage = ClientStorage(StorageKeys.accounts);
final values = accounts.map((final a) => json.encode(a.toJson())).toList();
await storage.setStringList(_keyAccounts, values);

232
packages/neon/neon/lib/src/blocs/apps.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();
}

232
packages/neon/neon/lib/src/blocs/clients.dart

@ -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();
}

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

@ -35,7 +35,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
}
final AccountsBloc _accountsBloc;
late final _storage = const AppStorage(StorageKeys.lastEndpoint);
late final _storage = const ClientStorage(StorageKeys.lastEndpoint);
final GlobalOptions _globalOptions;
StreamSubscription<List<Account>>? _accountsListener;

18
packages/neon/neon/lib/src/blocs/unified_search.dart

@ -6,7 +6,7 @@ import 'package:meta/meta.dart';
import 'package:neon/models.dart';
import 'package:neon/src/bloc/bloc.dart';
import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:rxdart/rxdart.dart';
@ -29,17 +29,17 @@ abstract interface class UnifiedSearchBlocStates {
@internal
class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEvents, UnifiedSearchBlocStates {
UnifiedSearchBloc(
this._appsBloc,
this._clientsBloc,
this._account,
) {
_appsBloc.activeApp.listen((final _) {
_clientsBloc.activeClient.listen((final _) {
if (enabled.value) {
disable();
}
});
}
final AppsBloc _appsBloc;
final ClientsBloc _clientsBloc;
final Account _account;
String _term = '';
@ -141,19 +141,19 @@ class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEven
Iterable<MapEntry<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>>> _sortResults(
final Map<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>> results,
) sync* {
final activeApp = _appsBloc.activeApp.value;
final activeClient = _clientsBloc.activeClient.value;
yield* results.entries
.where((final entry) => _providerMatchesApp(entry.key, activeApp))
.where((final entry) => _providerMatchesClient(entry.key, activeClient))
.sorted((final a, final b) => _sortEntriesCount(a.value, b.value));
yield* results.entries
.whereNot((final entry) => _providerMatchesApp(entry.key, activeApp))
.whereNot((final entry) => _providerMatchesClient(entry.key, activeClient))
.where((final entry) => _hasEntries(entry.value))
.sorted((final a, final b) => _sortEntriesCount(a.value, b.value));
}
bool _providerMatchesApp(final core.UnifiedSearchProvider provider, final AppImplementation app) =>
provider.id == app.id || provider.id.startsWith('${app.id}_');
bool _providerMatchesClient(final core.UnifiedSearchProvider provider, final ClientImplementation client) =>
provider.id == client.id || provider.id.startsWith('${client.id}_');
bool _hasEntries(final Result<core.UnifiedSearchResult> result) =>
!result.hasData || result.requireData.entries.isNotEmpty;

26
packages/neon/neon/lib/src/models/app_implementation.dart → packages/neon/neon/lib/src/models/client_implementation.dart

@ -20,24 +20,24 @@ import 'package:rxdart/rxdart.dart';
import 'package:vector_graphics/vector_graphics.dart';
@immutable
abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions> implements Disposable {
abstract class ClientImplementation<T extends Bloc, R extends NextcloudClientOptions> implements Disposable {
String get id;
LocalizationsDelegate<Object> get localizationsDelegate;
Iterable<Locale> get supportedLocales;
String nameFromLocalization(final NeonLocalizations localizations) => localizations.appImplementationName(id);
String nameFromLocalization(final NeonLocalizations localizations) => localizations.clientImplementationName(id);
String name(final BuildContext context) => nameFromLocalization(NeonLocalizations.of(context));
@protected
late final AppStorage storage = AppStorage(StorageKeys.apps, id);
late final ClientStorage storage = ClientStorage(StorageKeys.clients, id);
@mustBeOverridden
R get options;
/// Checks if the app is supported on the server of the [account].
/// Checks if the client is supported on the server of the [account].
///
/// A `supported` value of `null` means that it can not be known if the app is supported.
/// This is the case for apps that depend on the server version like files and we assume that the app is supported.
/// A `supported` value of `null` means that it can not be known if the client is supported.
/// This is the case for clients that depend on the server version like files and we assume that the client is supported.
/// The server support is handled differently.
///
/// The first value of the record is the supported status and the second value is the supported minimum version.
@ -87,13 +87,13 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
],
);
/// Route for the app.
/// Route for the client.
///
/// All pages of the app must be specified as subroutes.
/// All pages of the client must be specified as subroutes.
/// If this is not [GoRoute] an initial route name must be specified by overriding [initialRouteName].
RouteBase get route;
/// Name of the initial route for this app.
/// Name of the initial route for this client.
///
/// Subclasses that don't provide a [GoRoute] for [route] must override this.
String get initialRouteName {
@ -139,13 +139,13 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
final ThemeExtension? theme = null;
@override
bool operator ==(final Object other) => other is AppImplementation && other.id == id;
bool operator ==(final Object other) => other is ClientImplementation && other.id == id;
@override
int get hashCode => id.hashCode;
}
extension AppImplementationFind on Iterable<AppImplementation> {
AppImplementation? tryFind(final String? appID) => firstWhereOrNull((final app) => app.id == appID);
AppImplementation find(final String appID) => firstWhere((final app) => app.id == appID);
extension ClientImplementationFind on Iterable<ClientImplementation> {
ClientImplementation? tryFind(final String? clientID) => firstWhereOrNull((final client) => client.id == clientID);
ClientImplementation find(final String clientID) => firstWhere((final client) => client.id == clientID);
}

10
packages/neon/neon/lib/src/models/notifications_interface.dart

@ -1,11 +1,11 @@
import 'package:meta/meta.dart';
import 'package:neon/src/bloc/bloc.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/settings/models/options_collection.dart';
abstract interface class NotificationsAppInterface<T extends NotificationsBlocInterface,
R extends NotificationsOptionsInterface> extends AppImplementation<T, R> {
NotificationsAppInterface();
abstract interface class NotificationsClientInterface<T extends NotificationsBlocInterface,
R extends NotificationsOptionsInterface> extends ClientImplementation<T, R> {
NotificationsClientInterface();
@override
@mustBeOverridden
@ -19,6 +19,6 @@ abstract interface class NotificationsBlocInterface extends InteractiveBloc {
void deleteNotification(final int id);
}
abstract interface class NotificationsOptionsInterface extends NextcloudAppOptions {
abstract interface class NotificationsOptionsInterface extends NextcloudClientOptions {
NotificationsOptionsInterface(super.storage);
}

2
packages/neon/neon/lib/src/pages/account_settings.dart

@ -125,7 +125,7 @@ class AccountSettingsPage extends StatelessWidget {
title: Text(NeonLocalizations.of(context).optionsCategoryGeneral),
tiles: [
SelectSettingsTile(
option: options.initialApp,
option: options.initialClient,
),
],
),

24
packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart → packages/neon/neon/lib/src/pages/client_settings.dart

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/settings/widgets/option_settings_tile.dart';
import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:neon/src/settings/widgets/settings_list.dart';
@ -10,29 +10,29 @@ import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/confirmation_dialog.dart';
@internal
class NextcloudAppSettingsPage extends StatelessWidget {
const NextcloudAppSettingsPage({
required this.appImplementation,
class NextcloudClientSettingsPage extends StatelessWidget {
const NextcloudClientSettingsPage({
required this.clientImplementation,
super.key,
});
final AppImplementation appImplementation;
final ClientImplementation clientImplementation;
@override
Widget build(final BuildContext context) {
final appBar = AppBar(
title: Text(appImplementation.name(context)),
title: Text(clientImplementation.name(context)),
actions: [
IconButton(
onPressed: () async {
if (await showConfirmationDialog(
context,
NeonLocalizations.of(context).settingsResetForConfirmation(appImplementation.name(context)),
NeonLocalizations.of(context).settingsResetForConfirmation(clientImplementation.name(context)),
)) {
appImplementation.options.reset();
clientImplementation.options.reset();
}
},
tooltip: NeonLocalizations.of(context).settingsResetFor(appImplementation.name(context)),
tooltip: NeonLocalizations.of(context).settingsResetFor(clientImplementation.name(context)),
icon: const Icon(MdiIcons.cogRefresh),
),
],
@ -40,15 +40,15 @@ class NextcloudAppSettingsPage extends StatelessWidget {
final body = SettingsList(
categories: [
for (final category in [...appImplementation.options.categories, null]) ...[
if (appImplementation.options.options.where((final option) => option.category == category).isNotEmpty) ...[
for (final category in [...clientImplementation.options.categories, null]) ...[
if (clientImplementation.options.options.where((final option) => option.category == category).isNotEmpty) ...[
SettingsCategory(
title: Text(
category != null ? category.name(context) : NeonLocalizations.of(context).optionsCategoryOther,
),
tiles: [
for (final option
in appImplementation.options.options.where((final option) => option.category == category)) ...[
in clientImplementation.options.options.where((final option) => option.category == category)) ...[
OptionSettingsTile(option: option),
],
],

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

@ -5,9 +5,9 @@ import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/utils/global_options.dart';
import 'package:neon/src/utils/global_options.dart' as global_options;
import 'package:neon/src/utils/global_popups.dart';
@ -35,7 +35,7 @@ class _HomePageState extends State<HomePage> {
late Account _account;
late GlobalOptions _globalOptions;
late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc;
late ClientsBloc _clientsBloc;
late StreamSubscription<Map<String, String?>> _versionCheckSubscription;
@override
@ -44,9 +44,9 @@ class _HomePageState extends State<HomePage> {
_globalOptions = NeonProvider.of<GlobalOptions>(context);
_accountsBloc = NeonProvider.of<AccountsBloc>(context);
_account = _accountsBloc.activeAccount.value!;
_appsBloc = _accountsBloc.activeAppsBloc;
_clientsBloc = _accountsBloc.activeClientsBloc;
_versionCheckSubscription = _appsBloc.appVersions.listen((final values) {
_versionCheckSubscription = _clientsBloc.clientVersions.listen((final values) {
if (!mounted) {
return;
}
@ -56,12 +56,12 @@ class _HomePageState extends State<HomePage> {
final buffer = StringBuffer()..writeln();
for (final error in values.entries) {
final appId = error.key;
final clientId = error.key;
final minVersion = error.value;
final appName = l10n.appImplementationName(appId);
final clientName = l10n.clientImplementationName(clientId);
if (appName.isNotEmpty && minVersion != null) {
buffer.writeln('- $appName $minVersion');
if (clientName.isNotEmpty && minVersion != null) {
buffer.writeln('- $clientName $minVersion');
}
}
@ -126,20 +126,20 @@ class _HomePageState extends State<HomePage> {
const drawer = NeonDrawer();
const appBar = NeonAppBar();
final appView = StreamBuilder(
final clientView = StreamBuilder(
stream: _accountsBloc.activeUnifiedSearchBloc.enabled,
builder: (final context, final unifiedSearchEnabledSnapshot) {
if (unifiedSearchEnabledSnapshot.data ?? false) {
return const NeonUnifiedSearchResults();
}
return ResultBuilder<Iterable<AppImplementation>>.behaviorSubject(
subject: _appsBloc.appImplementations,
builder: (final context, final appImplementations) {
if (!appImplementations.hasData) {
return ResultBuilder<Iterable<ClientImplementation>>.behaviorSubject(
subject: _clientsBloc.clientImplementations,
builder: (final context, final clientImplementations) {
if (!clientImplementations.hasData) {
return const SizedBox();
}
if (appImplementations.requireData.isEmpty) {
if (clientImplementations.requireData.isEmpty) {
return Center(
child: Text(
NeonLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
@ -149,13 +149,13 @@ class _HomePageState extends State<HomePage> {
}
return StreamBuilder(
stream: _appsBloc.activeApp,
builder: (final context, final activeAppIDSnapshot) {
if (!activeAppIDSnapshot.hasData) {
stream: _clientsBloc.activeClient,
builder: (final context, final activeClientIDSnapshot) {
if (!activeClientIDSnapshot.hasData) {
return const SizedBox();
}
return activeAppIDSnapshot.requireData.page;
return activeClientIDSnapshot.requireData.page;
},
);
},
@ -173,7 +173,7 @@ class _HomePageState extends State<HomePage> {
resizeToAvoidBottomInset: false,
drawer: !drawerAlwaysVisible ? drawer : null,
appBar: appBar,
body: appView,
body: clientView,
);
if (drawerAlwaysVisible) {
@ -205,7 +205,7 @@ class _HomePageState extends State<HomePage> {
return false;
},
child: MultiProvider(
providers: _appsBloc.appBlocProviders,
providers: _clientsBloc.clientBlocProviders,
child: body,
),
);

26
packages/neon/neon/lib/src/pages/settings.dart

@ -4,7 +4,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/settings/utils/settings_export_helper.dart';
@ -28,7 +28,7 @@ import 'package:url_launcher/url_launcher_string.dart';
@internal
enum SettingsCategories {
apps,
clients,
theme,
navigation,
pushNotifications,
@ -56,7 +56,7 @@ class _SettingsPageState extends State<SettingsPage> {
Widget build(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
final branding = Branding.of(context);
final appBar = AppBar(
@ -67,8 +67,8 @@ class _SettingsPageState extends State<SettingsPage> {
if (await showConfirmationDialog(context, NeonLocalizations.of(context).settingsResetAllConfirmation)) {
globalOptions.reset();
for (final appImplementation in appImplementations) {
appImplementation.options.reset();
for (final clientImplementation in clientImplementations) {
clientImplementation.options.reset();
}
for (final account in accountsBloc.accounts.value) {
@ -100,15 +100,15 @@ class _SettingsPageState extends State<SettingsPage> {
categories: [
SettingsCategory(
title: Text(NeonLocalizations.of(context).settingsApps),
key: ValueKey(SettingsCategories.apps.name),
key: ValueKey(SettingsCategories.clients.name),
tiles: <SettingsTile>[
for (final appImplementation in appImplementations) ...[
if (appImplementation.options.options.isNotEmpty) ...[
for (final clientImplementation in clientImplementations) ...[
if (clientImplementation.options.options.isNotEmpty) ...[
CustomSettingsTile(
leading: appImplementation.buildIcon(),
title: Text(appImplementation.name(context)),
leading: clientImplementation.buildIcon(),
title: Text(clientImplementation.name(context)),
onTap: () {
NextcloudAppSettingsRoute(appid: appImplementation.id).go(context);
NextcloudClientSettingsRoute(clientid: clientImplementation.id).go(context);
},
),
],
@ -351,13 +351,13 @@ class _SettingsPageState extends State<SettingsPage> {
SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
return SettingsExportHelper(
exportables: {
globalOptions,
AccountsBlocExporter(accountsBloc),
AppImplementationsExporter(appImplementations),
ClientImplementationsExporter(clientImplementations),
},
);
}

24
packages/neon/neon/lib/src/router.dart

@ -8,15 +8,15 @@ import 'package:go_router/go_router.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/pages/account_settings.dart';
import 'package:neon/src/pages/client_settings.dart';
import 'package:neon/src/pages/home.dart';
import 'package:neon/src/pages/login.dart';
import 'package:neon/src/pages/login_check_account.dart';
import 'package:neon/src/pages/login_check_server_status.dart';
import 'package:neon/src/pages/login_flow.dart';
import 'package:neon/src/pages/login_qr_code.dart';
import 'package:neon/src/pages/nextcloud_app_settings.dart';
import 'package:neon/src/pages/route_not_found.dart';
import 'package:neon/src/pages/settings.dart';
import 'package:neon/src/utils/provider.dart';
@ -96,9 +96,9 @@ class AccountSettingsRoute extends GoRouteData {
path: 'settings',
name: 'Settings',
routes: [
TypedGoRoute<NextcloudAppSettingsRoute>(
path: 'apps/:appid',
name: 'NextcloudAppSettings',
TypedGoRoute<NextcloudClientSettingsRoute>(
path: 'apps/:clientid',
name: 'NextcloudClientSettings',
),
TypedGoRoute<_AddAccountRoute>(
path: 'account/add',
@ -365,19 +365,19 @@ class _AddAccountCheckAccountRoute extends LoginCheckAccountRoute {
}
@immutable
class NextcloudAppSettingsRoute extends GoRouteData {
const NextcloudAppSettingsRoute({
required this.appid,
class NextcloudClientSettingsRoute extends GoRouteData {
const NextcloudClientSettingsRoute({
required this.clientid,
});
final String appid;
final String clientid;
@override
Widget build(final BuildContext context, final GoRouterState state) {
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context);
final appImplementation = appImplementations.tryFind(appid)!;
final clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
final clientImplementation = clientImplementations.tryFind(clientid)!;
return NextcloudAppSettingsPage(appImplementation: appImplementation);
return NextcloudClientSettingsPage(clientImplementation: clientImplementation);
}
}

16
packages/neon/neon/lib/src/router.g.dart

@ -22,9 +22,9 @@ RouteBase get $homeRoute => GoRouteData.$route(
factory: $SettingsRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'apps/:appid',
name: 'NextcloudAppSettings',
factory: $NextcloudAppSettingsRouteExtension._fromState,
path: 'apps/:clientid',
name: 'NextcloudClientSettings',
factory: $NextcloudClientSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'account/add',
@ -98,7 +98,7 @@ extension $SettingsRouteExtension on SettingsRoute {
}
const _$SettingsCategoriesEnumMap = {
SettingsCategories.apps: 'apps',
SettingsCategories.clients: 'clients',
SettingsCategories.theme: 'theme',
SettingsCategories.navigation: 'navigation',
SettingsCategories.pushNotifications: 'push-notifications',
@ -108,13 +108,13 @@ const _$SettingsCategoriesEnumMap = {
SettingsCategories.other: 'other',
};
extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute(
appid: state.pathParameters['appid']!,
extension $NextcloudClientSettingsRouteExtension on NextcloudClientSettingsRoute {
static NextcloudClientSettingsRoute _fromState(GoRouterState state) => NextcloudClientSettingsRoute(
clientid: state.pathParameters['clientid']!,
);
String get location => GoRouteData.$location(
'/settings/apps/${Uri.encodeComponent(appid)}',
'/settings/apps/${Uri.encodeComponent(clientid)}',
);
void go(BuildContext context) => context.go(location);

8
packages/neon/neon/lib/src/settings/models/options_collection.dart

@ -11,7 +11,7 @@ abstract class OptionsCollection implements Exportable, Disposable {
/// Storage backend to use.
@protected
final AppStorage storage;
final ClientStorage storage;
/// Collection of options.
@protected
@ -56,9 +56,9 @@ abstract class OptionsCollection implements Exportable, Disposable {
}
}
/// OptionsCollection for a neon app.
abstract class NextcloudAppOptions extends OptionsCollection {
NextcloudAppOptions(super.storage);
/// OptionsCollection for a neon client.
abstract class NextcloudClientOptions extends OptionsCollection {
NextcloudClientOptions(super.storage);
/// Collection of categories to display the options in the settings.
late final Iterable<OptionsCategory> categories;

6
packages/neon/neon/lib/src/settings/models/storage.dart

@ -22,7 +22,7 @@ abstract interface class Storable {
@internal
enum StorageKeys implements Storable {
apps._('app'),
clients._('app'),
accounts._('accounts'),
global._('global'),
lastUsedAccount._('last-used-account'),
@ -100,8 +100,8 @@ final class SingleValueStorage {
@immutable
@internal
final class AppStorage implements SettingsStorage {
const AppStorage(
final class ClientStorage implements SettingsStorage {
const ClientStorage(
this.groupKey, [
this.suffix,
]);

22
packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart

@ -5,7 +5,7 @@ import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart' show Account, AccountFind;
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/settings/models/exportable.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/storage.dart';
@ -73,22 +73,22 @@ class SettingsExportHelper {
Map<String, Object?> exportToJson() => Map.fromEntries(exportables.map((final e) => e.export()));
}
/// Helper class to export [AppImplementation]s implementing the [Exportable] interface.
/// Helper class to export [ClientImplementation]s implementing the [Exportable] interface.
@internal
@immutable
class AppImplementationsExporter implements Exportable {
const AppImplementationsExporter(this.appImplementations);
class ClientImplementationsExporter implements Exportable {
const ClientImplementationsExporter(this.clientImplementations);
/// List of apps to export.
final Iterable<AppImplementation> appImplementations;
/// List of clients to export.
final Iterable<ClientImplementation> clientImplementations;
/// Key the exported value will be stored at.
static final _key = StorageKeys.apps.value;
static final _key = StorageKeys.clients.value;
@override
MapEntry<String, Object?> export() => MapEntry(
_key,
Map.fromEntries(appImplementations.map((final app) => app.options.export())),
Map.fromEntries(clientImplementations.map((final client) => client.options.export())),
);
@override
@ -100,10 +100,10 @@ class AppImplementationsExporter implements Exportable {
}
for (final element in values.entries) {
final app = appImplementations.tryFind(element.key);
final client = clientImplementations.tryFind(element.key);
if (app != null) {
app.options.import(values);
if (client != null) {
client.options.import(values);
}
}
}

6
packages/neon/neon/lib/src/theme/theme.dart

@ -13,13 +13,13 @@ class AppTheme {
required this.neonTheme,
final bool keepOriginalAccentColor = false,
this.oledAsDark = false,
this.appThemes,
this.clientThemes,
}) : keepOriginalAccentColor = nextcloudTheme == null || keepOriginalAccentColor;
final core.ThemingPublicCapabilities_Theming? nextcloudTheme;
final bool keepOriginalAccentColor;
final bool oledAsDark;
final Iterable<ThemeExtension>? appThemes;
final Iterable<ThemeExtension>? clientThemes;
final NeonTheme neonTheme;
ColorScheme _buildColorScheme(final Brightness brightness) {
@ -51,7 +51,7 @@ class AppTheme {
inputDecorationTheme: _inputDecorationTheme,
extensions: [
neonTheme,
...?appThemes,
...?clientThemes,
],
);
}

20
packages/neon/neon/lib/src/utils/account_options.dart

@ -1,6 +1,6 @@
import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/options_collection.dart';
import 'package:neon/src/settings/models/storage.dart';
@ -10,29 +10,29 @@ import 'package:neon/src/settings/models/storage.dart';
class AccountSpecificOptions extends OptionsCollection {
AccountSpecificOptions(
super.storage,
this._appsBloc,
this._clientsBloc,
) {
_appsBloc.appImplementations.listen((final result) {
_clientsBloc.clientImplementations.listen((final result) {
if (!result.hasData) {
return;
}
initialApp.values = {
initialClient.values = {
null: (final context) => NeonLocalizations.of(context).accountOptionsAutomatic,
}..addEntries(result.requireData.map((final app) => MapEntry(app.id, app.name)));
}..addEntries(result.requireData.map((final client) => MapEntry(client.id, client.name)));
});
}
final AppsBloc _appsBloc;
final ClientsBloc _clientsBloc;
@override
late final List<Option<dynamic>> options = [
initialApp,
initialClient,
];
late final initialApp = SelectOption<String?>(
late final initialClient = SelectOption<String?>(
storage: storage,
key: AccountOptionKeys.initialApp,
key: AccountOptionKeys.initialClient,
label: (final context) => NeonLocalizations.of(context).accountOptionsInitialApp,
defaultValue: null,
values: {},
@ -41,7 +41,7 @@ class AccountSpecificOptions extends OptionsCollection {
@internal
enum AccountOptionKeys implements Storable {
initialApp._('initial-app');
initialClient._('initial-app');
const AccountOptionKeys._(this.value);

14
packages/neon/neon/lib/src/utils/app_route.dart → packages/neon/neon/lib/src/utils/client_route.dart

@ -1,14 +1,14 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
/// [RouteData] for the initial page of an app.
/// [RouteData] for the initial page of a client.
///
/// Subclasses must override one of [build] or [redirect].
/// Routes should be prefixed with [appsBaseRoutePrefix].
/// Routes should be prefixed with [clientsBaseRoutePrefix].
@immutable
abstract class NeonBaseAppRoute extends GoRouteData {
/// Creates a new app base route.
const NeonBaseAppRoute();
abstract class NeonBaseClientRoute extends GoRouteData {
/// Creates a new client base route.
const NeonBaseClientRoute();
@override
Page<void> buildPage(final BuildContext context, final GoRouterState state) => NoTransitionPage(
@ -16,5 +16,5 @@ abstract class NeonBaseAppRoute extends GoRouteData {
);
}
/// Prefix for [NeonBaseAppRoute]s.
const appsBaseRoutePrefix = '/apps/';
/// Prefix for [NeonBaseClientRoute]s.
const clientsBaseRoutePrefix = '/apps/';

2
packages/neon/neon/lib/src/utils/global_options.dart

@ -18,7 +18,7 @@ const unifiedPushNextPushID = 'org.unifiedpush.distributor.nextpush';
class GlobalOptions extends OptionsCollection {
GlobalOptions(
this._packageInfo,
) : super(const AppStorage(StorageKeys.global)) {
) : super(const ClientStorage(StorageKeys.global)) {
pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener);
rememberLastUsedAccount.addListener(_rememberLastUsedAccountListener);
}

20
packages/neon/neon/lib/src/utils/push_utils.dart

@ -25,7 +25,7 @@ class PushUtils {
const PushUtils._();
static notifications.RSAKeypair loadRSAKeypair() {
const storage = AppStorage(StorageKeys.notifications);
const storage = ClientStorage(StorageKeys.notifications);
const keyDevicePrivateKey = 'device-private-key';
final notifications.RSAKeypair keypair;
@ -132,11 +132,11 @@ class PushUtils {
}
if (notification?.shouldNotify ?? true) {
final appID = notification?.app ?? pushNotification.subject.app ?? 'nextcloud';
String? appName = localizations.appImplementationName(appID);
if (appName.isEmpty) {
debugPrint('Missing app name for $appID');
appName = null;
final clientID = notification?.app ?? pushNotification.subject.app ?? 'nextcloud';
String? clientName = localizations.clientImplementationName(clientID);
if (clientName.isEmpty) {
debugPrint('Missing client name for $clientID');
clientName = null;
}
final title = (notification?.subject ?? pushNotification.subject.subject)!;
final message = (notification?.message.isNotEmpty ?? false) ? notification!.message : null;
@ -144,14 +144,14 @@ class PushUtils {
await localNotificationsPlugin.show(
_getNotificationID(instance, pushNotification),
message != null && appName != null ? '$appName: $title' : title,
message != null && clientName != null ? '$clientName: $title' : title,
message,
NotificationDetails(
android: AndroidNotificationDetails(
appID,
appName ?? appID,
clientID,
clientName ?? clientID,
subText: accounts.length > 1 && account != null ? account.humanReadableID : null,
groupKey: 'app_$appID',
groupKey: 'app_$clientID',
icon: '@mipmap/ic_launcher',
largeIcon: largeIconBitmap,
when: when?.millisecondsSinceEpoch,

66
packages/neon/neon/lib/src/widgets/app_bar.dart

@ -5,13 +5,13 @@ import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/models/notifications_interface.dart';
import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/widgets/account_switcher_button.dart';
import 'package:neon/src/widgets/app_implementation_icon.dart';
import 'package:neon/src/widgets/client_implementation_icon.dart';
import 'package:neon/src/widgets/error.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart';
import 'package:provider/provider.dart';
@ -32,7 +32,7 @@ class _NeonAppBarState extends State<NeonAppBar> {
late final AccountsBloc accountsBloc = NeonProvider.of<AccountsBloc>(context);
late final accounts = accountsBloc.accounts.value;
late final account = accountsBloc.activeAccount.value!;
late final appsBloc = accountsBloc.activeAppsBloc;
late final clientsBloc = accountsBloc.activeClientsBloc;
late final unifiedSearchBloc = accountsBloc.activeUnifiedSearchBloc;
final _searchBarFocusNode = FocusNode();
@ -62,11 +62,11 @@ class _NeonAppBarState extends State<NeonAppBar> {
}
@override
Widget build(final BuildContext context) => ResultBuilder<Iterable<AppImplementation>>.behaviorSubject(
subject: appsBloc.appImplementations,
builder: (final context, final appImplementations) => StreamBuilder(
stream: appsBloc.activeApp,
builder: (final context, final activeAppSnapshot) => StreamBuilder(
Widget build(final BuildContext context) => ResultBuilder<Iterable<ClientImplementation>>.behaviorSubject(
subject: clientsBloc.clientImplementations,
builder: (final context, final clientImplementations) => StreamBuilder(
stream: clientsBloc.activeClient,
builder: (final context, final activeClientSnapshot) => StreamBuilder(
stream: unifiedSearchBloc.enabled,
builder: (final context, final unifiedSearchEnabledSnapshot) {
final unifiedSearchEnabled = unifiedSearchEnabledSnapshot.data ?? false;
@ -78,24 +78,24 @@ class _NeonAppBarState extends State<NeonAppBar> {
children: [
Row(
children: [
if (activeAppSnapshot.hasData) ...[
if (activeClientSnapshot.hasData) ...[
Flexible(
child: Text(
activeAppSnapshot.requireData.name(context),
activeClientSnapshot.requireData.name(context),
),
),
],
if (appImplementations.hasError) ...[
if (clientImplementations.hasError) ...[
const SizedBox(
width: 8,
),
NeonError(
appImplementations.error,
onRetry: appsBloc.refresh,
clientImplementations.error,
onRetry: clientsBloc.refresh,
onlyIcon: true,
),
],
if (appImplementations.isLoading) ...[
if (clientImplementations.isLoading) ...[
const SizedBox(
width: 8,
),
@ -179,7 +179,7 @@ class NotificationIconButton extends StatefulWidget {
class _NotificationIconButtonState extends State<NotificationIconButton> {
late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc;
late ClientsBloc _clientsBloc;
late List<Account> _accounts;
late Account _account;
late StreamSubscription<void> notificationSubscription;
@ -188,14 +188,14 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
void initState() {
super.initState();
_accountsBloc = NeonProvider.of<AccountsBloc>(context);
_appsBloc = _accountsBloc.activeAppsBloc;
_clientsBloc = _accountsBloc.activeClientsBloc;
_accounts = _accountsBloc.accounts.value;
_account = _accountsBloc.activeAccount.value!;
notificationSubscription = _appsBloc.openNotifications.listen((final _) async {
final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull;
if (notificationsAppImplementation != null && notificationsAppImplementation.hasData) {
await _openNotifications(notificationsAppImplementation.data!);
notificationSubscription = _clientsBloc.openNotifications.listen((final _) async {
final notificationsClientImplementation = _clientsBloc.notificationsClientImplementation.valueOrNull;
if (notificationsClientImplementation != null && notificationsClientImplementation.hasData) {
await _openNotifications(notificationsClientImplementation.data!);
}
});
}
@ -209,7 +209,7 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
// TODO: migrate to go_router with a separate page
Future<void> _openNotifications(
final NotificationsAppInterface app,
final NotificationsClientInterface client,
) async {
final page = Scaffold(
resizeToAvoidBottomInset: false,
@ -217,7 +217,7 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(app.name(context)),
Text(client.name(context)),
if (_accounts.length > 1) ...[
Text(
_account.humanReadableID,
@ -227,13 +227,13 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
],
),
),
body: app.page,
body: client.page,
);
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (final context) => Provider<NotificationsBlocInterface>(
create: (final context) => app.getBloc(_account),
create: (final context) => client.getBloc(_account),
child: page,
),
),
@ -241,14 +241,14 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
}
@override
Widget build(final BuildContext context) => ResultBuilder<NotificationsAppInterface?>.behaviorSubject(
subject: _appsBloc.notificationsAppImplementation,
builder: (final context, final notificationsAppImplementation) {
if (!notificationsAppImplementation.hasData) {
Widget build(final BuildContext context) => ResultBuilder<NotificationsClientInterface?>.behaviorSubject(
subject: _clientsBloc.notificationsClientImplementation,
builder: (final context, final notificationsClientImplementation) {
if (!notificationsClientImplementation.hasData) {
return const SizedBox.shrink();
}
final notificationsImplementationData = notificationsAppImplementation.data!;
final notificationsImplementationData = notificationsClientImplementation.data!;
final notificationBloc = notificationsImplementationData.getBloc(_account);
return IconButton(
@ -256,11 +256,11 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
onPressed: () async {
await _openNotifications(notificationsImplementationData);
},
tooltip: NeonLocalizations.of(context).appImplementationName(notificationsImplementationData.id),
tooltip: NeonLocalizations.of(context).clientImplementationName(notificationsImplementationData.id),
icon: StreamBuilder<int>(
stream: notificationsImplementationData.getUnreadCounter(notificationBloc),
builder: (final context, final unreadCounterSnapshot) => NeonAppImplementationIcon(
appImplementation: notificationsImplementationData,
builder: (final context, final unreadCounterSnapshot) => NeonClientImplementationIcon(
clientImplementation: notificationsImplementationData,
unreadCount: unreadCounterSnapshot.data,
),
),

12
packages/neon/neon/lib/src/widgets/app_implementation_icon.dart → packages/neon/neon/lib/src/widgets/client_implementation_icon.dart

@ -1,18 +1,18 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
@internal
class NeonAppImplementationIcon extends StatelessWidget {
const NeonAppImplementationIcon({
required this.appImplementation,
class NeonClientImplementationIcon extends StatelessWidget {
const NeonClientImplementationIcon({
required this.clientImplementation,
this.unreadCount,
this.color,
this.size,
super.key,
});
final AppImplementation appImplementation;
final ClientImplementation clientImplementation;
final int? unreadCount;
@ -29,7 +29,7 @@ class NeonAppImplementationIcon extends StatelessWidget {
final icon = Container(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
child: clientImplementation.buildIcon(
size: size,
color: color,
),

48
packages/neon/neon/lib/src/widgets/drawer.dart

@ -5,8 +5,8 @@ import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/router.dart';
import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/widgets/drawer_destination.dart';
@ -24,17 +24,17 @@ class NeonDrawer extends StatelessWidget {
@override
Widget build(final BuildContext context) {
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appsBloc = accountsBloc.activeAppsBloc;
final clientsBloc = accountsBloc.activeClientsBloc;
return ResultBuilder.behaviorSubject(
subject: appsBloc.appImplementations,
subject: clientsBloc.clientImplementations,
builder: (final context, final snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return _NeonDrawer(
apps: snapshot.requireData,
clients: snapshot.requireData,
);
},
);
@ -43,10 +43,10 @@ class NeonDrawer extends StatelessWidget {
class _NeonDrawer extends StatefulWidget {
const _NeonDrawer({
required this.apps,
required this.clients,
});
final Iterable<AppImplementation> apps;
final Iterable<ClientImplementation> clients;
@override
State<_NeonDrawer> createState() => __NeonDrawerState();
@ -54,53 +54,53 @@ class _NeonDrawer extends StatefulWidget {
class __NeonDrawerState extends State<_NeonDrawer> {
late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc;
late List<AppImplementation> _apps;
late ClientsBloc _clientsBloc;
late List<ClientImplementation> _clients;
late int _activeApp;
late int _activeClient;
@override
void initState() {
super.initState();
_accountsBloc = NeonProvider.of<AccountsBloc>(context);
_appsBloc = _accountsBloc.activeAppsBloc;
_clientsBloc = _accountsBloc.activeClientsBloc;
_apps = widget.apps.toList();
_activeApp = _apps.indexWhere((final app) => app.id == _appsBloc.activeApp.valueOrNull?.id);
_clients = widget.clients.toList();
_activeClient = _clients.indexWhere((final client) => client.id == _clientsBloc.activeClient.valueOrNull?.id);
}
void onAppChange(final int index) {
void onDestinationSelected(final int index) {
Scaffold.maybeOf(context)?.closeDrawer();
// selected item is not a registered app like the SettingsPage
if (index >= _apps.length) {
// selected item is not a registered client like the SettingsPage
if (index >= _clients.length) {
const SettingsRoute().go(context);
return;
}
setState(() {
_activeApp = index;
_activeClient = index;
});
unawaited(_appsBloc.setActiveApp(_apps[index].id));
unawaited(_clientsBloc.setActiveClient(_clients[index].id));
//context.goNamed(apps[index].routeName);
}
@override
Widget build(final BuildContext context) {
final appDestinations = _apps.map(
(final app) => NavigationDrawerDestinationExtension.fromNeonDestination(
app.destination(context),
final clientDestinations = _clients.map(
(final client) => NavigationDrawerDestinationExtension.fromNeonDestination(
client.destination(context),
),
);
final drawer = NavigationDrawer(
selectedIndex: _activeApp,
onDestinationSelected: onAppChange,
selectedIndex: _activeClient,
onDestinationSelected: onDestinationSelected,
children: [
const NeonDrawerHeader(),
...appDestinations,
...clientDestinations,
NavigationDrawerDestination(
icon: const Icon(Icons.settings),
label: Text(NeonLocalizations.of(context).settings),

2
packages/neon/neon/lib/utils.dart

@ -1,5 +1,5 @@
export 'package:neon/l10n/localizations.dart';
export 'package:neon/src/utils/app_route.dart';
export 'package:neon/src/utils/client_route.dart';
export 'package:neon/src/utils/confirmation_dialog.dart';
export 'package:neon/src/utils/exceptions.dart';
export 'package:neon/src/utils/hex_color.dart';

30
packages/neon/neon/test/app_implementation_test.dart

@ -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));
});
});
}

30
packages/neon/neon/test/client_implementation_test.dart

@ -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));
});
});
}

4
packages/neon/neon/test/options_collection_test.dart

@ -6,8 +6,8 @@ import 'package:test/test.dart';
// ignore: missing_override_of_must_be_overridden
class OptionMock extends Mock implements ToggleOption {}
class Collection extends NextcloudAppOptions {
Collection(final List<Option<Object>> options) : super(const AppStorage(StorageKeys.apps)) {
class Collection extends NextcloudClientOptions {
Collection(final List<Option<Object>> options) : super(const ClientStorage(StorageKeys.clients)) {
super.options = options;
}
}

10
packages/neon/neon/test/settings_export_test.dart

@ -4,7 +4,7 @@ import 'dart:typed_data';
import 'package:mocktail/mocktail.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/settings/models/exportable.dart';
import 'package:neon/src/settings/models/options_collection.dart';
import 'package:neon/src/settings/utils/settings_export_helper.dart';
@ -13,9 +13,9 @@ import 'package:rxdart/rxdart.dart';
import 'package:test/test.dart';
// ignore: missing_override_of_must_be_overridden, avoid_implementing_value_types
class FakeAppImplementation extends Mock implements AppImplementation {}
class FakeAppImplementation extends Mock implements ClientImplementation {}
class NextcloudAppOptionsMock extends Mock implements NextcloudAppOptions {}
class NextcloudAppOptionsMock extends Mock implements NextcloudClientOptions {}
class AccountsBlocMock extends Mock implements AccountsBloc {}
@ -29,14 +29,14 @@ class ExporterMock extends Mock implements Exportable {}
void main() {
group('Exporter', () {
test('AccountsBlocExporter', () {
var exporter = const AppImplementationsExporter([]);
var exporter = const ClientImplementationsExporter([]);
var export = exporter.export();
expect(Map.fromEntries([export]), {'app': <String, dynamic>{}});
final fakeApp = FakeAppImplementation();
final fakeOptions = NextcloudAppOptionsMock();
exporter = AppImplementationsExporter([fakeApp]);
exporter = ClientImplementationsExporter([fakeApp]);
const appValue = MapEntry('appID', 'value');
const appExport = {

6
packages/neon/neon/test/storage_test.dart

@ -17,12 +17,12 @@ void main() {
group('AppStorage', () {
test('formatKey', () async {
var appStorage = const AppStorage(StorageKeys.accounts);
var appStorage = const ClientStorage(StorageKeys.accounts);
var key = appStorage.formatKey('test-key');
expect(key, 'accounts-test-key');
expect(appStorage.id, StorageKeys.accounts.value);
appStorage = const AppStorage(StorageKeys.accounts, 'test-suffix');
appStorage = const ClientStorage(StorageKeys.accounts, 'test-suffix');
key = appStorage.formatKey('test-key');
expect(key, 'accounts-test-suffix-test-key');
expect(appStorage.id, 'test-suffix');
@ -31,7 +31,7 @@ void main() {
test('interface', () async {
final sharedPreferences = SharedPreferencesMock();
NeonStorage.mock(sharedPreferences);
const appStorage = AppStorage(StorageKeys.accounts);
const appStorage = ClientStorage(StorageKeys.accounts);
const key = 'key';
final formattedKey = appStorage.formatKey(key);

10
packages/neon/neon_dashboard/lib/src/app.dart

@ -10,9 +10,9 @@ import 'package:nextcloud/core.dart' as core;
import 'package:nextcloud/nextcloud.dart';
/// Implementation of the server `dashboard` app.
class DashboardApp extends AppImplementation<DashboardBloc, DashboardAppSpecificOptions> {
/// Creates a new Dashboard app implementation instance.
DashboardApp();
class DashboardClient extends ClientImplementation<DashboardBloc, DashboardClientSpecificOptions> {
/// Creates a new Dashboard client implementation instance.
DashboardClient();
@override
final String id = AppIDs.dashboard;
@ -24,7 +24,7 @@ class DashboardApp extends AppImplementation<DashboardBloc, DashboardAppSpecific
final List<Locale> supportedLocales = DashboardLocalizations.supportedLocales;
@override
late final DashboardAppSpecificOptions options = DashboardAppSpecificOptions(storage);
late final DashboardClientSpecificOptions options = DashboardClientSpecificOptions(storage);
@override
DashboardBloc buildBloc(final Account account) => DashboardBloc(account);
@ -33,7 +33,7 @@ class DashboardApp extends AppImplementation<DashboardBloc, DashboardAppSpecific
final Widget page = const DashboardMainPage();
@override
final RouteBase route = $dashboardAppRoute;
final RouteBase route = $dashboardClientRoute;
@override
(bool?, String?) isSupported(

6
packages/neon/neon_dashboard/lib/src/options.dart

@ -1,9 +1,9 @@
import 'package:neon/settings.dart';
/// Settings options specific to the dashboard app.
class DashboardAppSpecificOptions extends NextcloudAppOptions {
/// Settings options specific to the dashboard client.
class DashboardClientSpecificOptions extends NextcloudClientOptions {
/// Creates a new dashboard options instance.
DashboardAppSpecificOptions(super.storage) {
DashboardClientSpecificOptions(super.storage) {
super.categories = [];
super.options = [];
}

12
packages/neon/neon_dashboard/lib/src/routes.dart

@ -6,15 +6,15 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart';
/// Route for the dashboard app.
@TypedGoRoute<DashboardAppRoute>(
path: '$appsBaseRoutePrefix${AppIDs.dashboard}',
/// Route for the dashboard client.
@TypedGoRoute<DashboardClientRoute>(
path: '$clientsBaseRoutePrefix${AppIDs.dashboard}',
name: AppIDs.dashboard,
)
@immutable
class DashboardAppRoute extends NeonBaseAppRoute {
/// Creates a new dashboard app route.
const DashboardAppRoute();
class DashboardClientRoute extends NeonBaseClientRoute {
/// Creates a new dashboard client route.
const DashboardClientRoute();
@override
Widget build(final BuildContext context, final GoRouterState state) => const DashboardMainPage();

10
packages/neon/neon_dashboard/lib/src/routes.g.dart

@ -7,17 +7,17 @@ part of 'routes.dart';
// **************************************************************************
List<RouteBase> get $appRoutes => [
$dashboardAppRoute,
$dashboardClientRoute,
];
RouteBase get $dashboardAppRoute => GoRouteData.$route(
RouteBase get $dashboardClientRoute => GoRouteData.$route(
path: '/apps/dashboard',
name: 'dashboard',
factory: $DashboardAppRouteExtension._fromState,
factory: $DashboardClientRouteExtension._fromState,
);
extension $DashboardAppRouteExtension on DashboardAppRoute {
static DashboardAppRoute _fromState(GoRouterState state) => const DashboardAppRoute();
extension $DashboardClientRouteExtension on DashboardClientRoute {
static DashboardClientRoute _fromState(GoRouterState state) => const DashboardClientRoute();
String get location => GoRouteData.$location(
'/apps/dashboard',

2
packages/neon/neon_files/lib/blocs/browser.dart

@ -25,7 +25,7 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
unawaited(refresh());
}
final FilesAppSpecificOptions options;
final FilesClientSpecificOptions options;
final Account account;
@override

2
packages/neon/neon_files/lib/blocs/files.dart

@ -35,7 +35,7 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
options.downloadQueueParallelism.addListener(_downloadParallelismListener);
}
final FilesAppSpecificOptions options;
final FilesClientSpecificOptions options;
final Account account;
late final browser = getNewFilesBrowserBloc();

8
packages/neon/neon_files/lib/neon_files.dart

@ -46,8 +46,8 @@ part 'widgets/browser_view.dart';
part 'widgets/file_preview.dart';
part 'widgets/navigator.dart';
class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
FilesApp();
class FilesClient extends ClientImplementation<FilesBloc, FilesClientSpecificOptions> {
FilesClient();
@override
final String id = AppIDs.files;
@ -59,7 +59,7 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
final List<Locale> supportedLocales = FilesLocalizations.supportedLocales;
@override
late final FilesAppSpecificOptions options = FilesAppSpecificOptions(storage);
late final FilesClientSpecificOptions options = FilesClientSpecificOptions(storage);
@override
FilesBloc buildBloc(final Account account) => FilesBloc(
@ -71,7 +71,7 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
final Widget page = const FilesMainPage();
@override
final RouteBase route = $filesAppRoute;
final RouteBase route = $filesClientRoute;
@override
(bool? supported, String? minimumVersion) isSupported(

4
packages/neon/neon_files/lib/options.dart

@ -1,7 +1,7 @@
part of 'neon_files.dart';
class FilesAppSpecificOptions extends NextcloudAppOptions {
FilesAppSpecificOptions(super.storage) {
class FilesClientSpecificOptions extends NextcloudClientOptions {
FilesClientSpecificOptions(super.storage) {
super.categories = [
generalCategory,
];

8
packages/neon/neon_files/lib/routes.dart

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart';
@TypedGoRoute<FilesAppRoute>(
path: '$appsBaseRoutePrefix${AppIDs.files}',
@TypedGoRoute<FilesClientRoute>(
path: '$clientsBaseRoutePrefix${AppIDs.files}',
name: AppIDs.files,
)
@immutable
class FilesAppRoute extends NeonBaseAppRoute {
const FilesAppRoute();
class FilesClientRoute extends NeonBaseClientRoute {
const FilesClientRoute();
@override
Widget build(final BuildContext context, final GoRouterState state) => const FilesMainPage();

10
packages/neon/neon_files/lib/routes.g.dart

@ -7,17 +7,17 @@ part of 'routes.dart';
// **************************************************************************
List<RouteBase> get $appRoutes => [
$filesAppRoute,
$filesClientRoute,
];
RouteBase get $filesAppRoute => GoRouteData.$route(
RouteBase get $filesClientRoute => GoRouteData.$route(
path: '/apps/files',
name: 'files',
factory: $FilesAppRouteExtension._fromState,
factory: $FilesClientRouteExtension._fromState,
);
extension $FilesAppRouteExtension on FilesAppRoute {
static FilesAppRoute _fromState(GoRouterState state) => const FilesAppRoute();
extension $FilesClientRouteExtension on FilesClientRoute {
static FilesClientRoute _fromState(GoRouterState state) => const FilesClientRoute();
String get location => GoRouteData.$location(
'/apps/files',

2
packages/neon/neon_news/lib/blocs/articles.dart

@ -56,7 +56,7 @@ class NewsArticlesBloc extends InteractiveBloc implements NewsArticlesBlocEvents
}
final NewsBloc _newsBloc;
final NewsAppSpecificOptions options;
final NewsClientSpecificOptions options;
final Account account;
final int? id;
final ListType? listType;

2
packages/neon/neon_news/lib/blocs/news.dart

@ -47,7 +47,7 @@ class NewsBloc extends InteractiveBloc implements NewsBlocEvents, NewsBlocStates
@override
NewsBloc get _newsBloc => this;
@override
final NewsAppSpecificOptions options;
final NewsClientSpecificOptions options;
@override
@override
final Account account;

8
packages/neon/neon_news/lib/neon_news.dart

@ -54,8 +54,8 @@ part 'widgets/folder_select.dart';
part 'widgets/folder_view.dart';
part 'widgets/folders_view.dart';
class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
NewsApp();
class NewsClient extends ClientImplementation<NewsBloc, NewsClientSpecificOptions> {
NewsClient();
@override
final String id = AppIDs.news;
@ -67,7 +67,7 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
final List<Locale> supportedLocales = NewsLocalizations.supportedLocales;
@override
late final NewsAppSpecificOptions options = NewsAppSpecificOptions(storage);
late final NewsClientSpecificOptions options = NewsClientSpecificOptions(storage);
@override
NewsBloc buildBloc(final Account account) => NewsBloc(
@ -79,7 +79,7 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
final Widget page = const NewsMainPage();
@override
final RouteBase route = $newsAppRoute;
final RouteBase route = $newsClientRoute;
@override
BehaviorSubject<int> getUnreadCounter(final NewsBloc bloc) => bloc.unreadCounter;

4
packages/neon/neon_news/lib/options.dart

@ -1,7 +1,7 @@
part of 'neon_news.dart';
class NewsAppSpecificOptions extends NextcloudAppOptions {
NewsAppSpecificOptions(super.storage) {
class NewsClientSpecificOptions extends NextcloudClientOptions {
NewsClientSpecificOptions(super.storage) {
super.categories = [
generalCategory,
articlesCategory,

8
packages/neon/neon_news/lib/routes.dart

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart';
@TypedGoRoute<NewsAppRoute>(
path: '$appsBaseRoutePrefix${AppIDs.news}',
@TypedGoRoute<NewsClientRoute>(
path: '$clientsBaseRoutePrefix${AppIDs.news}',
name: AppIDs.news,
)
@immutable
class NewsAppRoute extends NeonBaseAppRoute {
const NewsAppRoute();
class NewsClientRoute extends NeonBaseClientRoute {
const NewsClientRoute();
@override
Widget build(final BuildContext context, final GoRouterState state) => const NewsMainPage();

10
packages/neon/neon_news/lib/routes.g.dart

@ -7,17 +7,17 @@ part of 'routes.dart';
// **************************************************************************
List<RouteBase> get $appRoutes => [
$newsAppRoute,
$newsClientRoute,
];
RouteBase get $newsAppRoute => GoRouteData.$route(
RouteBase get $newsClientRoute => GoRouteData.$route(
path: '/apps/news',
name: 'news',
factory: $NewsAppRouteExtension._fromState,
factory: $NewsClientRouteExtension._fromState,
);
extension $NewsAppRouteExtension on NewsAppRoute {
static NewsAppRoute _fromState(GoRouterState state) => const NewsAppRoute();
extension $NewsClientRouteExtension on NewsClientRoute {
static NewsClientRoute _fromState(GoRouterState state) => const NewsClientRoute();
String get location => GoRouteData.$location(
'/apps/news',

2
packages/neon/neon_notes/lib/blocs/note.dart

@ -43,7 +43,7 @@ class NotesNoteBloc extends InteractiveBloc implements NotesNoteBlocEvents, Note
});
}
late final NotesAppSpecificOptions options = _notesBloc.options;
late final NotesClientSpecificOptions options = _notesBloc.options;
final NotesBloc _notesBloc;
final _updateQueue = Queue();

2
packages/neon/neon_notes/lib/blocs/notes.dart

@ -30,7 +30,7 @@ class NotesBloc extends InteractiveBloc implements NotesBlocEvents, NotesBlocSta
unawaited(refresh());
}
final NotesAppSpecificOptions options;
final NotesClientSpecificOptions options;
final Account account;
@override

8
packages/neon/neon_notes/lib/neon_notes.dart

@ -43,8 +43,8 @@ part 'widgets/category_select.dart';
part 'widgets/notes_floating_action_button.dart';
part 'widgets/notes_view.dart';
class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
NotesApp();
class NotesClient extends ClientImplementation<NotesBloc, NotesClientSpecificOptions> {
NotesClient();
@override
final String id = AppIDs.notes;
@ -56,7 +56,7 @@ class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
final LocalizationsDelegate<NotesLocalizations> localizationsDelegate = NotesLocalizations.delegate;
@override
late final NotesAppSpecificOptions options = NotesAppSpecificOptions(storage);
late final NotesClientSpecificOptions options = NotesClientSpecificOptions(storage);
@override
NotesBloc buildBloc(final Account account) => NotesBloc(
@ -68,7 +68,7 @@ class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
final Widget page = const NotesMainPage();
@override
final RouteBase route = $notesAppRoute;
final RouteBase route = $notesClientRoute;
@override
(bool? supported, String? minimumVersion) isSupported(

4
packages/neon/neon_notes/lib/options.dart

@ -1,7 +1,7 @@
part of 'neon_notes.dart';
class NotesAppSpecificOptions extends NextcloudAppOptions {
NotesAppSpecificOptions(super.storage) {
class NotesClientSpecificOptions extends NextcloudClientOptions {
NotesClientSpecificOptions(super.storage) {
super.categories = [
generalCategory,
notesCategory,

8
packages/neon/neon_notes/lib/routes.dart

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart';
@TypedGoRoute<NotesAppRoute>(
path: '$appsBaseRoutePrefix${AppIDs.notes}',
@TypedGoRoute<NotesClientRoute>(
path: '$clientsBaseRoutePrefix${AppIDs.notes}',
name: AppIDs.notes,
)
@immutable
class NotesAppRoute extends NeonBaseAppRoute {
const NotesAppRoute();
class NotesClientRoute extends NeonBaseClientRoute {
const NotesClientRoute();
@override
Widget build(final BuildContext context, final GoRouterState state) => const NotesMainPage();

10
packages/neon/neon_notes/lib/routes.g.dart

@ -7,17 +7,17 @@ part of 'routes.dart';
// **************************************************************************
List<RouteBase> get $appRoutes => [
$notesAppRoute,
$notesClientRoute,
];
RouteBase get $notesAppRoute => GoRouteData.$route(
RouteBase get $notesClientRoute => GoRouteData.$route(
path: '/apps/notes',
name: 'notes',
factory: $NotesAppRouteExtension._fromState,
factory: $NotesClientRouteExtension._fromState,
);
extension $NotesAppRouteExtension on NotesAppRoute {
static NotesAppRoute _fromState(GoRouterState state) => const NotesAppRoute();
extension $NotesClientRouteExtension on NotesClientRoute {
static NotesClientRoute _fromState(GoRouterState state) => const NotesClientRoute();
String get location => GoRouteData.$location(
'/apps/notes',

2
packages/neon/neon_notifications/lib/blocs/notifications.dart

@ -29,7 +29,7 @@ class NotificationsBloc extends InteractiveBloc
}
@override
final NotificationsAppSpecificOptions options;
final NotificationsClientSpecificOptions options;
final Account _account;
late final NeonTimer _timer;

10
packages/neon/neon_notifications/lib/neon_notifications.dart

@ -22,11 +22,11 @@ part 'blocs/notifications.dart';
part 'options.dart';
part 'pages/main.dart';
class NotificationsApp extends AppImplementation<NotificationsBloc, NotificationsAppSpecificOptions>
class NotificationsClient extends ClientImplementation<NotificationsBloc, NotificationsClientSpecificOptions>
implements
// ignore: avoid_implementing_value_types
NotificationsAppInterface<NotificationsBloc, NotificationsAppSpecificOptions> {
NotificationsApp();
NotificationsClientInterface<NotificationsBloc, NotificationsClientSpecificOptions> {
NotificationsClient();
@override
final String id = AppIDs.notifications;
@ -38,7 +38,7 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
final List<Locale> supportedLocales = NotificationsLocalizations.supportedLocales;
@override
late final NotificationsAppSpecificOptions options = NotificationsAppSpecificOptions(storage);
late final NotificationsClientSpecificOptions options = NotificationsClientSpecificOptions(storage);
@override
NotificationsBloc buildBloc(final Account account) => NotificationsBloc(
@ -50,7 +50,7 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
final Widget page = const NotificationsMainPage();
@override
final RouteBase route = $notificationsAppRoute;
final RouteBase route = $notificationsClientRoute;
@override
BehaviorSubject<int> getUnreadCounter(final NotificationsBloc bloc) => bloc.unreadCounter;

4
packages/neon/neon_notifications/lib/options.dart

@ -1,7 +1,7 @@
part of 'neon_notifications.dart';
class NotificationsAppSpecificOptions extends NextcloudAppOptions implements NotificationsOptionsInterface {
NotificationsAppSpecificOptions(super.storage) {
class NotificationsClientSpecificOptions extends NextcloudClientOptions implements NotificationsOptionsInterface {
NotificationsClientSpecificOptions(super.storage) {
super.categories = [];
super.options = [];
}

10
packages/neon/neon_notifications/lib/pages/main.dart

@ -54,7 +54,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
final BuildContext context,
final notifications.Notification notification,
) {
final app = NeonProvider.of<Iterable<AppImplementation>>(context).tryFind(notification.app);
final client = NeonProvider.of<Iterable<ClientImplementation>>(context).tryFind(notification.app);
return ListTile(
title: Text(notification.subject),
@ -75,8 +75,8 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
),
],
),
leading: app != null
? app.buildIcon(
leading: client != null
? client.buildIcon(
size: largeIconSize,
)
: SizedBox.fromSize(
@ -91,10 +91,10 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
if (notification.app == AppIDs.notifications) {
return;
}
if (app != null) {
if (client != null) {
// TODO: use go_router once implemented
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
await accountsBloc.activeAppsBloc.setActiveApp(app.id);
await accountsBloc.activeClientsBloc.setActiveClient(client.id);
} else {
final colorScheme = Theme.of(context).colorScheme;

8
packages/neon/neon_notifications/lib/routes.dart

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart';
@TypedGoRoute<NotificationsAppRoute>(
path: '$appsBaseRoutePrefix${AppIDs.notifications}',
@TypedGoRoute<NotificationsClientRoute>(
path: '$clientsBaseRoutePrefix${AppIDs.notifications}',
name: AppIDs.notifications,
)
@immutable
class NotificationsAppRoute extends NeonBaseAppRoute {
const NotificationsAppRoute();
class NotificationsClientRoute extends NeonBaseClientRoute {
const NotificationsClientRoute();
@override
Widget build(final BuildContext context, final GoRouterState state) => const NotificationsMainPage();

10
packages/neon/neon_notifications/lib/routes.g.dart

@ -7,17 +7,17 @@ part of 'routes.dart';
// **************************************************************************
List<RouteBase> get $appRoutes => [
$notificationsAppRoute,
$notificationsClientRoute,
];
RouteBase get $notificationsAppRoute => GoRouteData.$route(
RouteBase get $notificationsClientRoute => GoRouteData.$route(
path: '/apps/notifications',
name: 'notifications',
factory: $NotificationsAppRouteExtension._fromState,
factory: $NotificationsClientRouteExtension._fromState,
);
extension $NotificationsAppRouteExtension on NotificationsAppRoute {
static NotificationsAppRoute _fromState(GoRouterState state) => const NotificationsAppRoute();
extension $NotificationsClientRouteExtension on NotificationsClientRoute {
static NotificationsClientRoute _fromState(GoRouterState state) => const NotificationsClientRoute();
String get location => GoRouteData.$location(
'/apps/notifications',

Loading…
Cancel
Save