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 crypton
exportables exportables
fcmup fcmup

2
packages/app/integration_test/screenshot_test.dart

@ -24,7 +24,7 @@ Future<void> runTestApp(
final Account? account, final Account? account,
}) async { }) async {
await runNeon( await runNeon(
appImplementations: appImplementations, clientImplementations: clientImplementations,
theme: neonTheme, theme: neonTheme,
bindingOverride: binding, bindingOverride: binding,
account: account, 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'; import 'package:neon_notifications/neon_notifications.dart';
/// The collection of clients enabled for the Neon app. /// The collection of clients enabled for the Neon app.
final Set<AppImplementation> appImplementations = { final Set<ClientImplementation> clientImplementations = {
DashboardApp(), DashboardClient(),
FilesApp(), FilesClient(),
NewsApp(), NewsClient(),
NotesApp(), NotesClient(),
NotificationsApp(), NotificationsClient(),
}; };

2
packages/app/lib/main.dart

@ -4,7 +4,7 @@ import 'package:neon/neon.dart';
Future<void> main() async { Future<void> main() async {
await runNeon( await runNeon(
appImplementations: appImplementations, clientImplementations: clientImplementations,
theme: neonTheme, 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/bloc.dart';
export 'package:neon/src/bloc/result.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/accounts.dart' show AccountsBloc;
export 'package:neon/src/blocs/timer.dart' hide TimerBlocEvents, TimerBlocStates; export 'package:neon/src/blocs/timer.dart' hide TimerBlocEvents, TimerBlocStates;

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

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

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

@ -101,11 +101,11 @@ abstract class NeonLocalizations {
/// **'Nextcloud logo'** /// **'Nextcloud logo'**
String get nextcloudLogo; String get nextcloudLogo;
/// No description provided for @appImplementationName. /// No description provided for @clientImplementationName.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'{app, select, nextcloud{Nextcloud} core{Server} dashboard{Dashboard} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}'** /// **'{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. /// 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'; String get nextcloudLogo => 'Nextcloud logo';
@override @override
String appImplementationName(String app) { String clientImplementationName(String app) {
String _temp0 = intl.Intl.selectLogic( String _temp0 = intl.Intl.selectLogic(
app, 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/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'; 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/next_push.dart';
import 'package:neon/src/blocs/push_notifications.dart'; import 'package:neon/src/blocs/push_notifications.dart';
import 'package:neon/src/models/account.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/models/disposable.dart';
import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/settings/models/storage.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'; import 'package:provider/provider.dart';
Future<void> runNeon({ Future<void> runNeon({
required final Set<AppImplementation> appImplementations, required final Set<ClientImplementation> clientImplementations,
required final NeonTheme theme, required final NeonTheme theme,
@visibleForTesting final WidgetsBinding? bindingOverride, @visibleForTesting final WidgetsBinding? bindingOverride,
@visibleForTesting final Account? account, @visibleForTesting final Account? account,
@ -44,7 +44,7 @@ Future<void> runNeon({
final accountsBloc = AccountsBloc( final accountsBloc = AccountsBloc(
globalOptions, globalOptions,
appImplementations, clientImplementations,
); );
if (account != null) { if (account != null) {
accountsBloc accountsBloc
@ -71,9 +71,9 @@ Future<void> runNeon({
NeonProvider<AccountsBloc>.value(value: accountsBloc), NeonProvider<AccountsBloc>.value(value: accountsBloc),
NeonProvider<FirstLaunchBloc>.value(value: firstLaunchBloc), NeonProvider<FirstLaunchBloc>.value(value: firstLaunchBloc),
NeonProvider<NextPushBloc>.value(value: nextPushBloc), NeonProvider<NextPushBloc>.value(value: nextPushBloc),
Provider<Iterable<AppImplementation>>( Provider<Iterable<ClientImplementation>>(
create: (final _) => appImplementations, create: (final _) => clientImplementations,
dispose: (final _, final appImplementations) => appImplementations.disposeAll(), dispose: (final _, final clientImplementations) => clientImplementations.disposeAll(),
), ),
Provider<PackageInfo>.value(value: packageInfo), 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/bloc/result.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.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/notifications_interface.dart';
import 'package:neon/src/models/push_notification.dart'; import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/platform/platform.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 { class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.TrayListener, WindowListener {
final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true); final _appRegex = RegExp(r'^app_([a-z]+)$', multiLine: true);
final _navigatorKey = GlobalKey<NavigatorState>(); final _navigatorKey = GlobalKey<NavigatorState>();
late final Iterable<AppImplementation> _appImplementations; late final Iterable<ClientImplementation> _clientImplementations;
late final GlobalOptions _globalOptions; late final GlobalOptions _globalOptions;
late final AccountsBloc _accountsBloc; late final AccountsBloc _accountsBloc;
late final _routerDelegate = buildAppRouter( late final _routerDelegate = buildAppRouter(
@ -60,7 +60,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
void initState() { void initState() {
super.initState(); super.initState();
_appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context); _clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
_globalOptions = NeonProvider.of<GlobalOptions>(context); _globalOptions = NeonProvider.of<GlobalOptions>(context);
_accountsBloc = NeonProvider.of<AccountsBloc>(context); _accountsBloc = NeonProvider.of<AccountsBloc>(context);
@ -81,12 +81,12 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
if (NeonPlatform.instance.canUseQuickActions) { if (NeonPlatform.instance.canUseQuickActions) {
const quickActions = QuickActions(); const quickActions = QuickActions();
await quickActions.setShortcutItems( await quickActions.setShortcutItems(
_appImplementations _clientImplementations
.map( .map(
(final app) => ShortcutItem( (final client) => ShortcutItem(
type: 'app_${app.id}', type: 'app_${client.id}',
localizedTitle: app.nameFromLocalization(localizations), localizedTitle: client.nameFromLocalization(localizations),
icon: 'app_${app.id}', icon: 'app_${client.id}',
), ),
) )
.toList(), .toList(),
@ -111,10 +111,10 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
await tray.trayManager.setContextMenu( await tray.trayManager.setContextMenu(
tray.Menu( tray.Menu(
items: [ items: [
for (final app in _appImplementations) ...[ for (final client in _clientImplementations) ...[
tray.MenuItem( tray.MenuItem(
key: 'app_${app.id}', key: 'app_${client.id}',
label: app.nameFromLocalization(localizations), label: client.nameFromLocalization(localizations),
// TODO: Add icons which should work on macOS and Windows // TODO: Add icons which should work on macOS and Windows
), ),
], ],
@ -145,14 +145,14 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
return; return;
} }
final allAppImplementations = NeonProvider.of<Iterable<AppImplementation>>(context); final allClientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
final app = allAppImplementations.tryFind(AppIDs.notifications) as NotificationsAppInterface?; final client = allClientImplementations.tryFind(AppIDs.notifications) as NotificationsClientInterface?;
if (app == null) { if (client == null) {
return; return;
} }
await _accountsBloc.getAppsBlocFor(account).getAppBloc<NotificationsBlocInterface>(app).refresh(); await _accountsBloc.getClientsBlocFor(account).getClientBloc<NotificationsBlocInterface>(client).refresh();
}; };
Global.onPushNotificationClicked = (final pushNotificationWithAccountID) async { Global.onPushNotificationClicked = (final pushNotificationWithAccountID) async {
final account = _accountsBloc.accounts.value.tryFind(pushNotificationWithAccountID.accountID); final account = _accountsBloc.accounts.value.tryFind(pushNotificationWithAccountID.accountID);
@ -161,22 +161,24 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
} }
_accountsBloc.setActiveAccount(account); _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?; final notificationsClient =
if (notificationsApp != null) { allClientImplementations.tryFind(AppIDs.notifications) as NotificationsClientInterface?;
if (notificationsClient != null) {
_accountsBloc _accountsBloc
.getAppsBlocFor(account) .getClientsBlocFor(account)
.getAppBloc<NotificationsBlocInterface>(notificationsApp) .getClientBloc<NotificationsBlocInterface>(notificationsClient)
.deleteNotification(pushNotificationWithAccountID.subject.nid!); .deleteNotification(pushNotificationWithAccountID.subject.nid!);
} }
final app = allAppImplementations.tryFind(pushNotificationWithAccountID.subject.app) ?? notificationsApp; final client =
if (app == null) { allClientImplementations.tryFind(pushNotificationWithAccountID.subject.app) ?? notificationsClient;
if (client == null) {
return; return;
} }
await _openAppFromExternal(account, app.id); await _openAppFromExternal(account, client.id);
}; };
final details = await localNotificationsPlugin.getNotificationAppLaunchDetails(); 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 { 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'); _navigatorKey.currentState!.popUntil((final route) => route.settings.name == 'home');
await _showAndRestoreWindow(); await _showAndRestoreWindow();
} }
@ -291,17 +293,19 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
capabilitiesSnapshot.data?.capabilities.themingPublicCapabilities?.theming, capabilitiesSnapshot.data?.capabilities.themingPublicCapabilities?.theming,
keepOriginalAccentColor: options.themeKeepOriginalAccentColor.value, keepOriginalAccentColor: options.themeKeepOriginalAccentColor.value,
oledAsDark: options.themeOLEDAsDark.value, oledAsDark: options.themeOLEDAsDark.value,
appThemes: _appImplementations.map((final a) => a.theme).whereNotNull(), clientThemes: _clientImplementations.map((final c) => c.theme).whereNotNull(),
neonTheme: widget.neonTheme, neonTheme: widget.neonTheme,
); );
return MaterialApp.router( return MaterialApp.router(
localizationsDelegates: [ localizationsDelegates: [
..._appImplementations.map((final app) => app.localizationsDelegate), ..._clientImplementations.map((final client) => client.localizationsDelegate),
...NeonLocalizations.localizationsDelegates, ...NeonLocalizations.localizationsDelegates,
], ],
supportedLocales: { supportedLocales: {
..._appImplementations.map((final app) => app.supportedLocales).expand((final element) => element), ..._clientImplementations
.map((final client) => client.supportedLocales)
.expand((final element) => element),
...NeonLocalizations.supportedLocales, ...NeonLocalizations.supportedLocales,
}, },
themeMode: options.themeMode.value, 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:flutter/foundation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/bloc/bloc.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/capabilities.dart';
import 'package:neon/src/blocs/clients.dart';
import 'package:neon/src/blocs/unified_search.dart'; import 'package:neon/src/blocs/unified_search.dart';
import 'package:neon/src/blocs/user_details.dart'; import 'package:neon/src/blocs/user_details.dart';
import 'package:neon/src/blocs/user_statuses.dart'; import 'package:neon/src/blocs/user_statuses.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/account_cache.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/settings/models/storage.dart';
import 'package:neon/src/utils/account_options.dart'; import 'package:neon/src/utils/account_options.dart';
import 'package:neon/src/utils/global_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 { class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocStates {
AccountsBloc( AccountsBloc(
this._globalOptions, this._globalOptions,
this._allAppImplementations, this._allClientImplementations,
) { ) {
const lastUsedStorage = SingleValueStorage(StorageKeys.lastUsedAccount); const lastUsedStorage = SingleValueStorage(StorageKeys.lastUsedAccount);
@ -103,22 +103,22 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
accounts.listen((final accounts) { accounts.listen((final accounts) {
_accountsOptions.pruneAgainst(accounts); _accountsOptions.pruneAgainst(accounts);
_appsBlocs.pruneAgainst(accounts); _clientsBlocs.pruneAgainst(accounts);
_capabilitiesBlocs.pruneAgainst(accounts); _capabilitiesBlocs.pruneAgainst(accounts);
_userDetailsBlocs.pruneAgainst(accounts); _userDetailsBlocs.pruneAgainst(accounts);
_userStatusesBlocs.pruneAgainst(accounts); _userStatusesBlocs.pruneAgainst(accounts);
_unifiedSearchBlocs.pruneAgainst(accounts); _unifiedSearchBlocs.pruneAgainst(accounts);
for (final app in _allAppImplementations) { for (final client in _allClientImplementations) {
app.blocsCache.pruneAgainst(accounts); client.blocsCache.pruneAgainst(accounts);
} }
}); });
} }
final GlobalOptions _globalOptions; final GlobalOptions _globalOptions;
final Iterable<AppImplementation> _allAppImplementations; final Iterable<ClientImplementation> _allClientImplementations;
final _accountsOptions = AccountCache<AccountSpecificOptions>(); final _accountsOptions = AccountCache<AccountSpecificOptions>();
final _appsBlocs = AccountCache<AppsBloc>(); final _clientsBlocs = AccountCache<ClientsBloc>();
final _capabilitiesBlocs = AccountCache<CapabilitiesBloc>(); final _capabilitiesBlocs = AccountCache<CapabilitiesBloc>();
final _userDetailsBlocs = AccountCache<UserDetailsBloc>(); final _userDetailsBlocs = AccountCache<UserDetailsBloc>();
final _userStatusesBlocs = AccountCache<UserStatusesBloc>(); final _userStatusesBlocs = AccountCache<UserStatusesBloc>();
@ -128,7 +128,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
void dispose() { void dispose() {
unawaited(activeAccount.close()); unawaited(activeAccount.close());
unawaited(accounts.close()); unawaited(accounts.close());
_appsBlocs.dispose(); _clientsBlocs.dispose();
_capabilitiesBlocs.dispose(); _capabilitiesBlocs.dispose();
_userDetailsBlocs.dispose(); _userDetailsBlocs.dispose();
_userStatusesBlocs.dispose(); _userStatusesBlocs.dispose();
@ -226,23 +226,23 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// ///
/// Use [activeOptions] to get them for the [activeAccount]. /// Use [activeOptions] to get them for the [activeAccount].
AccountSpecificOptions getOptionsFor(final Account account) => _accountsOptions[account] ??= AccountSpecificOptions( AccountSpecificOptions getOptionsFor(final Account account) => _accountsOptions[account] ??= AccountSpecificOptions(
AppStorage(StorageKeys.accounts, account.id), ClientStorage(StorageKeys.accounts, account.id),
getAppsBlocFor(account), getClientsBlocFor(account),
); );
/// The appsBloc for the [activeAccount]. /// The clientsBloc for the [activeAccount].
/// ///
/// Convenience method for [getAppsBlocFor] with the currently active account. /// Convenience method for [getClientsBlocFor] with the currently active account.
AppsBloc get activeAppsBloc => getAppsBlocFor(aa); 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]. /// Use [activeClientsBloc] to get them for the [activeAccount].
AppsBloc getAppsBlocFor(final Account account) => _appsBlocs[account] ??= AppsBloc( ClientsBloc getClientsBlocFor(final Account account) => _clientsBlocs[account] ??= ClientsBloc(
getCapabilitiesBlocFor(account), getCapabilitiesBlocFor(account),
this, this,
account, account,
_allAppImplementations, _allClientImplementations,
); );
/// The capabilitiesBloc for the [activeAccount]. /// The capabilitiesBloc for the [activeAccount].
@ -288,7 +288,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// Use [activeUnifiedSearchBloc] to get them for the [activeAccount]. /// Use [activeUnifiedSearchBloc] to get them for the [activeAccount].
UnifiedSearchBloc getUnifiedSearchBlocFor(final Account account) => UnifiedSearchBloc getUnifiedSearchBlocFor(final Account account) =>
_unifiedSearchBlocs[account] ??= UnifiedSearchBloc( _unifiedSearchBlocs[account] ??= UnifiedSearchBloc(
getAppsBlocFor(account), getClientsBlocFor(account),
account, account,
); );
} }
@ -297,7 +297,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// ///
/// It is not checked whether the stored information is still valid. /// It is not checked whether the stored information is still valid.
List<Account> loadAccounts() { List<Account> loadAccounts() {
const storage = AppStorage(StorageKeys.accounts); const storage = ClientStorage(StorageKeys.accounts);
if (storage.containsKey(_keyAccounts)) { if (storage.containsKey(_keyAccounts)) {
return storage return storage
@ -310,7 +310,7 @@ List<Account> loadAccounts() {
/// Saves the given [accounts] to the storage. /// Saves the given [accounts] to the storage.
Future<void> saveAccounts(final List<Account> accounts) async { 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(); final values = accounts.map((final a) => json.encode(a.toJson())).toList();
await storage.setStringList(_keyAccounts, values); 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; final AccountsBloc _accountsBloc;
late final _storage = const AppStorage(StorageKeys.lastEndpoint); late final _storage = const ClientStorage(StorageKeys.lastEndpoint);
final GlobalOptions _globalOptions; final GlobalOptions _globalOptions;
StreamSubscription<List<Account>>? _accountsListener; 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/models.dart';
import 'package:neon/src/bloc/bloc.dart'; import 'package:neon/src/bloc/bloc.dart';
import 'package:neon/src/bloc/result.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:nextcloud/core.dart' as core;
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@ -29,17 +29,17 @@ abstract interface class UnifiedSearchBlocStates {
@internal @internal
class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEvents, UnifiedSearchBlocStates { class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEvents, UnifiedSearchBlocStates {
UnifiedSearchBloc( UnifiedSearchBloc(
this._appsBloc, this._clientsBloc,
this._account, this._account,
) { ) {
_appsBloc.activeApp.listen((final _) { _clientsBloc.activeClient.listen((final _) {
if (enabled.value) { if (enabled.value) {
disable(); disable();
} }
}); });
} }
final AppsBloc _appsBloc; final ClientsBloc _clientsBloc;
final Account _account; final Account _account;
String _term = ''; String _term = '';
@ -141,19 +141,19 @@ class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEven
Iterable<MapEntry<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>>> _sortResults( Iterable<MapEntry<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>>> _sortResults(
final Map<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>> results, final Map<core.UnifiedSearchProvider, Result<core.UnifiedSearchResult>> results,
) sync* { ) sync* {
final activeApp = _appsBloc.activeApp.value; final activeClient = _clientsBloc.activeClient.value;
yield* results.entries 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)); .sorted((final a, final b) => _sortEntriesCount(a.value, b.value));
yield* results.entries yield* results.entries
.whereNot((final entry) => _providerMatchesApp(entry.key, activeApp)) .whereNot((final entry) => _providerMatchesClient(entry.key, activeClient))
.where((final entry) => _hasEntries(entry.value)) .where((final entry) => _hasEntries(entry.value))
.sorted((final a, final b) => _sortEntriesCount(a.value, b.value)); .sorted((final a, final b) => _sortEntriesCount(a.value, b.value));
} }
bool _providerMatchesApp(final core.UnifiedSearchProvider provider, final AppImplementation app) => bool _providerMatchesClient(final core.UnifiedSearchProvider provider, final ClientImplementation client) =>
provider.id == app.id || provider.id.startsWith('${app.id}_'); provider.id == client.id || provider.id.startsWith('${client.id}_');
bool _hasEntries(final Result<core.UnifiedSearchResult> result) => bool _hasEntries(final Result<core.UnifiedSearchResult> result) =>
!result.hasData || result.requireData.entries.isNotEmpty; !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'; import 'package:vector_graphics/vector_graphics.dart';
@immutable @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; String get id;
LocalizationsDelegate<Object> get localizationsDelegate; LocalizationsDelegate<Object> get localizationsDelegate;
Iterable<Locale> get supportedLocales; 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)); String name(final BuildContext context) => nameFromLocalization(NeonLocalizations.of(context));
@protected @protected
late final AppStorage storage = AppStorage(StorageKeys.apps, id); late final ClientStorage storage = ClientStorage(StorageKeys.clients, id);
@mustBeOverridden @mustBeOverridden
R get options; 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. /// A `supported` value of `null` means that it can not be known if the client is supported.
/// This is the case for apps that depend on the server version like files and we assume that the app 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 server support is handled differently.
/// ///
/// The first value of the record is the supported status and the second value is the supported minimum version. /// 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]. /// If this is not [GoRoute] an initial route name must be specified by overriding [initialRouteName].
RouteBase get route; 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. /// Subclasses that don't provide a [GoRoute] for [route] must override this.
String get initialRouteName { String get initialRouteName {
@ -139,13 +139,13 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
final ThemeExtension? theme = null; final ThemeExtension? theme = null;
@override @override
bool operator ==(final Object other) => other is AppImplementation && other.id == id; bool operator ==(final Object other) => other is ClientImplementation && other.id == id;
@override @override
int get hashCode => id.hashCode; int get hashCode => id.hashCode;
} }
extension AppImplementationFind on Iterable<AppImplementation> { extension ClientImplementationFind on Iterable<ClientImplementation> {
AppImplementation? tryFind(final String? appID) => firstWhereOrNull((final app) => app.id == appID); ClientImplementation? tryFind(final String? clientID) => firstWhereOrNull((final client) => client.id == clientID);
AppImplementation find(final String appID) => firstWhere((final app) => app.id == appID); 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:meta/meta.dart';
import 'package:neon/src/bloc/bloc.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'; import 'package:neon/src/settings/models/options_collection.dart';
abstract interface class NotificationsAppInterface<T extends NotificationsBlocInterface, abstract interface class NotificationsClientInterface<T extends NotificationsBlocInterface,
R extends NotificationsOptionsInterface> extends AppImplementation<T, R> { R extends NotificationsOptionsInterface> extends ClientImplementation<T, R> {
NotificationsAppInterface(); NotificationsClientInterface();
@override @override
@mustBeOverridden @mustBeOverridden
@ -19,6 +19,6 @@ abstract interface class NotificationsBlocInterface extends InteractiveBloc {
void deleteNotification(final int id); void deleteNotification(final int id);
} }
abstract interface class NotificationsOptionsInterface extends NextcloudAppOptions { abstract interface class NotificationsOptionsInterface extends NextcloudClientOptions {
NotificationsOptionsInterface(super.storage); 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), title: Text(NeonLocalizations.of(context).optionsCategoryGeneral),
tiles: [ tiles: [
SelectSettingsTile( 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:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.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/option_settings_tile.dart';
import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_category.dart';
import 'package:neon/src/settings/widgets/settings_list.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'; import 'package:neon/src/utils/confirmation_dialog.dart';
@internal @internal
class NextcloudAppSettingsPage extends StatelessWidget { class NextcloudClientSettingsPage extends StatelessWidget {
const NextcloudAppSettingsPage({ const NextcloudClientSettingsPage({
required this.appImplementation, required this.clientImplementation,
super.key, super.key,
}); });
final AppImplementation appImplementation; final ClientImplementation clientImplementation;
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final appBar = AppBar( final appBar = AppBar(
title: Text(appImplementation.name(context)), title: Text(clientImplementation.name(context)),
actions: [ actions: [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
if (await showConfirmationDialog( if (await showConfirmationDialog(
context, 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), icon: const Icon(MdiIcons.cogRefresh),
), ),
], ],
@ -40,15 +40,15 @@ class NextcloudAppSettingsPage extends StatelessWidget {
final body = SettingsList( final body = SettingsList(
categories: [ categories: [
for (final category in [...appImplementation.options.categories, null]) ...[ for (final category in [...clientImplementation.options.categories, null]) ...[
if (appImplementation.options.options.where((final option) => option.category == category).isNotEmpty) ...[ if (clientImplementation.options.options.where((final option) => option.category == category).isNotEmpty) ...[
SettingsCategory( SettingsCategory(
title: Text( title: Text(
category != null ? category.name(context) : NeonLocalizations.of(context).optionsCategoryOther, category != null ? category.name(context) : NeonLocalizations.of(context).optionsCategoryOther,
), ),
tiles: [ tiles: [
for (final option 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), 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/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart'; import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.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/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';
import 'package:neon/src/utils/global_options.dart' as global_options; import 'package:neon/src/utils/global_options.dart' as global_options;
import 'package:neon/src/utils/global_popups.dart'; import 'package:neon/src/utils/global_popups.dart';
@ -35,7 +35,7 @@ class _HomePageState extends State<HomePage> {
late Account _account; late Account _account;
late GlobalOptions _globalOptions; late GlobalOptions _globalOptions;
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc; late ClientsBloc _clientsBloc;
late StreamSubscription<Map<String, String?>> _versionCheckSubscription; late StreamSubscription<Map<String, String?>> _versionCheckSubscription;
@override @override
@ -44,9 +44,9 @@ class _HomePageState extends State<HomePage> {
_globalOptions = NeonProvider.of<GlobalOptions>(context); _globalOptions = NeonProvider.of<GlobalOptions>(context);
_accountsBloc = NeonProvider.of<AccountsBloc>(context); _accountsBloc = NeonProvider.of<AccountsBloc>(context);
_account = _accountsBloc.activeAccount.value!; _account = _accountsBloc.activeAccount.value!;
_appsBloc = _accountsBloc.activeAppsBloc; _clientsBloc = _accountsBloc.activeClientsBloc;
_versionCheckSubscription = _appsBloc.appVersions.listen((final values) { _versionCheckSubscription = _clientsBloc.clientVersions.listen((final values) {
if (!mounted) { if (!mounted) {
return; return;
} }
@ -56,12 +56,12 @@ class _HomePageState extends State<HomePage> {
final buffer = StringBuffer()..writeln(); final buffer = StringBuffer()..writeln();
for (final error in values.entries) { for (final error in values.entries) {
final appId = error.key; final clientId = error.key;
final minVersion = error.value; final minVersion = error.value;
final appName = l10n.appImplementationName(appId); final clientName = l10n.clientImplementationName(clientId);
if (appName.isNotEmpty && minVersion != null) { if (clientName.isNotEmpty && minVersion != null) {
buffer.writeln('- $appName $minVersion'); buffer.writeln('- $clientName $minVersion');
} }
} }
@ -126,20 +126,20 @@ class _HomePageState extends State<HomePage> {
const drawer = NeonDrawer(); const drawer = NeonDrawer();
const appBar = NeonAppBar(); const appBar = NeonAppBar();
final appView = StreamBuilder( final clientView = StreamBuilder(
stream: _accountsBloc.activeUnifiedSearchBloc.enabled, stream: _accountsBloc.activeUnifiedSearchBloc.enabled,
builder: (final context, final unifiedSearchEnabledSnapshot) { builder: (final context, final unifiedSearchEnabledSnapshot) {
if (unifiedSearchEnabledSnapshot.data ?? false) { if (unifiedSearchEnabledSnapshot.data ?? false) {
return const NeonUnifiedSearchResults(); return const NeonUnifiedSearchResults();
} }
return ResultBuilder<Iterable<AppImplementation>>.behaviorSubject( return ResultBuilder<Iterable<ClientImplementation>>.behaviorSubject(
subject: _appsBloc.appImplementations, subject: _clientsBloc.clientImplementations,
builder: (final context, final appImplementations) { builder: (final context, final clientImplementations) {
if (!appImplementations.hasData) { if (!clientImplementations.hasData) {
return const SizedBox(); return const SizedBox();
} }
if (appImplementations.requireData.isEmpty) { if (clientImplementations.requireData.isEmpty) {
return Center( return Center(
child: Text( child: Text(
NeonLocalizations.of(context).errorNoCompatibleNextcloudAppsFound, NeonLocalizations.of(context).errorNoCompatibleNextcloudAppsFound,
@ -149,13 +149,13 @@ class _HomePageState extends State<HomePage> {
} }
return StreamBuilder( return StreamBuilder(
stream: _appsBloc.activeApp, stream: _clientsBloc.activeClient,
builder: (final context, final activeAppIDSnapshot) { builder: (final context, final activeClientIDSnapshot) {
if (!activeAppIDSnapshot.hasData) { if (!activeClientIDSnapshot.hasData) {
return const SizedBox(); return const SizedBox();
} }
return activeAppIDSnapshot.requireData.page; return activeClientIDSnapshot.requireData.page;
}, },
); );
}, },
@ -173,7 +173,7 @@ class _HomePageState extends State<HomePage> {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
drawer: !drawerAlwaysVisible ? drawer : null, drawer: !drawerAlwaysVisible ? drawer : null,
appBar: appBar, appBar: appBar,
body: appView, body: clientView,
); );
if (drawerAlwaysVisible) { if (drawerAlwaysVisible) {
@ -205,7 +205,7 @@ class _HomePageState extends State<HomePage> {
return false; return false;
}, },
child: MultiProvider( child: MultiProvider(
providers: _appsBloc.appBlocProviders, providers: _clientsBloc.clientBlocProviders,
child: body, 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:meta/meta.dart';
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/accounts.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/platform/platform.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/settings/utils/settings_export_helper.dart'; import 'package:neon/src/settings/utils/settings_export_helper.dart';
@ -28,7 +28,7 @@ import 'package:url_launcher/url_launcher_string.dart';
@internal @internal
enum SettingsCategories { enum SettingsCategories {
apps, clients,
theme, theme,
navigation, navigation,
pushNotifications, pushNotifications,
@ -56,7 +56,7 @@ class _SettingsPageState extends State<SettingsPage> {
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context); final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(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 branding = Branding.of(context);
final appBar = AppBar( final appBar = AppBar(
@ -67,8 +67,8 @@ class _SettingsPageState extends State<SettingsPage> {
if (await showConfirmationDialog(context, NeonLocalizations.of(context).settingsResetAllConfirmation)) { if (await showConfirmationDialog(context, NeonLocalizations.of(context).settingsResetAllConfirmation)) {
globalOptions.reset(); globalOptions.reset();
for (final appImplementation in appImplementations) { for (final clientImplementation in clientImplementations) {
appImplementation.options.reset(); clientImplementation.options.reset();
} }
for (final account in accountsBloc.accounts.value) { for (final account in accountsBloc.accounts.value) {
@ -100,15 +100,15 @@ class _SettingsPageState extends State<SettingsPage> {
categories: [ categories: [
SettingsCategory( SettingsCategory(
title: Text(NeonLocalizations.of(context).settingsApps), title: Text(NeonLocalizations.of(context).settingsApps),
key: ValueKey(SettingsCategories.apps.name), key: ValueKey(SettingsCategories.clients.name),
tiles: <SettingsTile>[ tiles: <SettingsTile>[
for (final appImplementation in appImplementations) ...[ for (final clientImplementation in clientImplementations) ...[
if (appImplementation.options.options.isNotEmpty) ...[ if (clientImplementation.options.options.isNotEmpty) ...[
CustomSettingsTile( CustomSettingsTile(
leading: appImplementation.buildIcon(), leading: clientImplementation.buildIcon(),
title: Text(appImplementation.name(context)), title: Text(clientImplementation.name(context)),
onTap: () { 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) { SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = NeonProvider.of<GlobalOptions>(context); final globalOptions = NeonProvider.of<GlobalOptions>(context);
final accountsBloc = NeonProvider.of<AccountsBloc>(context); final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context); final clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
return SettingsExportHelper( return SettingsExportHelper(
exportables: { exportables: {
globalOptions, globalOptions,
AccountsBlocExporter(accountsBloc), 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:meta/meta.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.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/account_settings.dart';
import 'package:neon/src/pages/client_settings.dart';
import 'package:neon/src/pages/home.dart'; import 'package:neon/src/pages/home.dart';
import 'package:neon/src/pages/login.dart'; import 'package:neon/src/pages/login.dart';
import 'package:neon/src/pages/login_check_account.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_check_server_status.dart';
import 'package:neon/src/pages/login_flow.dart'; import 'package:neon/src/pages/login_flow.dart';
import 'package:neon/src/pages/login_qr_code.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/route_not_found.dart';
import 'package:neon/src/pages/settings.dart'; import 'package:neon/src/pages/settings.dart';
import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/provider.dart';
@ -96,9 +96,9 @@ class AccountSettingsRoute extends GoRouteData {
path: 'settings', path: 'settings',
name: 'Settings', name: 'Settings',
routes: [ routes: [
TypedGoRoute<NextcloudAppSettingsRoute>( TypedGoRoute<NextcloudClientSettingsRoute>(
path: 'apps/:appid', path: 'apps/:clientid',
name: 'NextcloudAppSettings', name: 'NextcloudClientSettings',
), ),
TypedGoRoute<_AddAccountRoute>( TypedGoRoute<_AddAccountRoute>(
path: 'account/add', path: 'account/add',
@ -365,19 +365,19 @@ class _AddAccountCheckAccountRoute extends LoginCheckAccountRoute {
} }
@immutable @immutable
class NextcloudAppSettingsRoute extends GoRouteData { class NextcloudClientSettingsRoute extends GoRouteData {
const NextcloudAppSettingsRoute({ const NextcloudClientSettingsRoute({
required this.appid, required this.clientid,
}); });
final String appid; final String clientid;
@override @override
Widget build(final BuildContext context, final GoRouterState state) { Widget build(final BuildContext context, final GoRouterState state) {
final appImplementations = NeonProvider.of<Iterable<AppImplementation>>(context); final clientImplementations = NeonProvider.of<Iterable<ClientImplementation>>(context);
final appImplementation = appImplementations.tryFind(appid)!; 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, factory: $SettingsRouteExtension._fromState,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: 'apps/:appid', path: 'apps/:clientid',
name: 'NextcloudAppSettings', name: 'NextcloudClientSettings',
factory: $NextcloudAppSettingsRouteExtension._fromState, factory: $NextcloudClientSettingsRouteExtension._fromState,
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'account/add', path: 'account/add',
@ -98,7 +98,7 @@ extension $SettingsRouteExtension on SettingsRoute {
} }
const _$SettingsCategoriesEnumMap = { const _$SettingsCategoriesEnumMap = {
SettingsCategories.apps: 'apps', SettingsCategories.clients: 'clients',
SettingsCategories.theme: 'theme', SettingsCategories.theme: 'theme',
SettingsCategories.navigation: 'navigation', SettingsCategories.navigation: 'navigation',
SettingsCategories.pushNotifications: 'push-notifications', SettingsCategories.pushNotifications: 'push-notifications',
@ -108,13 +108,13 @@ const _$SettingsCategoriesEnumMap = {
SettingsCategories.other: 'other', SettingsCategories.other: 'other',
}; };
extension $NextcloudAppSettingsRouteExtension on NextcloudAppSettingsRoute { extension $NextcloudClientSettingsRouteExtension on NextcloudClientSettingsRoute {
static NextcloudAppSettingsRoute _fromState(GoRouterState state) => NextcloudAppSettingsRoute( static NextcloudClientSettingsRoute _fromState(GoRouterState state) => NextcloudClientSettingsRoute(
appid: state.pathParameters['appid']!, clientid: state.pathParameters['clientid']!,
); );
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/apps/${Uri.encodeComponent(appid)}', '/settings/apps/${Uri.encodeComponent(clientid)}',
); );
void go(BuildContext context) => context.go(location); 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. /// Storage backend to use.
@protected @protected
final AppStorage storage; final ClientStorage storage;
/// Collection of options. /// Collection of options.
@protected @protected
@ -56,9 +56,9 @@ abstract class OptionsCollection implements Exportable, Disposable {
} }
} }
/// OptionsCollection for a neon app. /// OptionsCollection for a neon client.
abstract class NextcloudAppOptions extends OptionsCollection { abstract class NextcloudClientOptions extends OptionsCollection {
NextcloudAppOptions(super.storage); NextcloudClientOptions(super.storage);
/// Collection of categories to display the options in the settings. /// Collection of categories to display the options in the settings.
late final Iterable<OptionsCategory> categories; late final Iterable<OptionsCategory> categories;

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

@ -22,7 +22,7 @@ abstract interface class Storable {
@internal @internal
enum StorageKeys implements Storable { enum StorageKeys implements Storable {
apps._('app'), clients._('app'),
accounts._('accounts'), accounts._('accounts'),
global._('global'), global._('global'),
lastUsedAccount._('last-used-account'), lastUsedAccount._('last-used-account'),
@ -100,8 +100,8 @@ final class SingleValueStorage {
@immutable @immutable
@internal @internal
final class AppStorage implements SettingsStorage { final class ClientStorage implements SettingsStorage {
const AppStorage( const ClientStorage(
this.groupKey, [ this.groupKey, [
this.suffix, 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:meta/meta.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart' show Account, AccountFind; 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/exportable.dart';
import 'package:neon/src/settings/models/option.dart'; import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/storage.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())); 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 @internal
@immutable @immutable
class AppImplementationsExporter implements Exportable { class ClientImplementationsExporter implements Exportable {
const AppImplementationsExporter(this.appImplementations); const ClientImplementationsExporter(this.clientImplementations);
/// List of apps to export. /// List of clients to export.
final Iterable<AppImplementation> appImplementations; final Iterable<ClientImplementation> clientImplementations;
/// Key the exported value will be stored at. /// Key the exported value will be stored at.
static final _key = StorageKeys.apps.value; static final _key = StorageKeys.clients.value;
@override @override
MapEntry<String, Object?> export() => MapEntry( MapEntry<String, Object?> export() => MapEntry(
_key, _key,
Map.fromEntries(appImplementations.map((final app) => app.options.export())), Map.fromEntries(clientImplementations.map((final client) => client.options.export())),
); );
@override @override
@ -100,10 +100,10 @@ class AppImplementationsExporter implements Exportable {
} }
for (final element in values.entries) { for (final element in values.entries) {
final app = appImplementations.tryFind(element.key); final client = clientImplementations.tryFind(element.key);
if (app != null) { if (client != null) {
app.options.import(values); client.options.import(values);
} }
} }
} }

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

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

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

@ -1,6 +1,6 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.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/option.dart';
import 'package:neon/src/settings/models/options_collection.dart'; import 'package:neon/src/settings/models/options_collection.dart';
import 'package:neon/src/settings/models/storage.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 { class AccountSpecificOptions extends OptionsCollection {
AccountSpecificOptions( AccountSpecificOptions(
super.storage, super.storage,
this._appsBloc, this._clientsBloc,
) { ) {
_appsBloc.appImplementations.listen((final result) { _clientsBloc.clientImplementations.listen((final result) {
if (!result.hasData) { if (!result.hasData) {
return; return;
} }
initialApp.values = { initialClient.values = {
null: (final context) => NeonLocalizations.of(context).accountOptionsAutomatic, 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 @override
late final List<Option<dynamic>> options = [ late final List<Option<dynamic>> options = [
initialApp, initialClient,
]; ];
late final initialApp = SelectOption<String?>( late final initialClient = SelectOption<String?>(
storage: storage, storage: storage,
key: AccountOptionKeys.initialApp, key: AccountOptionKeys.initialClient,
label: (final context) => NeonLocalizations.of(context).accountOptionsInitialApp, label: (final context) => NeonLocalizations.of(context).accountOptionsInitialApp,
defaultValue: null, defaultValue: null,
values: {}, values: {},
@ -41,7 +41,7 @@ class AccountSpecificOptions extends OptionsCollection {
@internal @internal
enum AccountOptionKeys implements Storable { enum AccountOptionKeys implements Storable {
initialApp._('initial-app'); initialClient._('initial-app');
const AccountOptionKeys._(this.value); 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:flutter/widgets.dart';
import 'package:go_router/go_router.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]. /// Subclasses must override one of [build] or [redirect].
/// Routes should be prefixed with [appsBaseRoutePrefix]. /// Routes should be prefixed with [clientsBaseRoutePrefix].
@immutable @immutable
abstract class NeonBaseAppRoute extends GoRouteData { abstract class NeonBaseClientRoute extends GoRouteData {
/// Creates a new app base route. /// Creates a new client base route.
const NeonBaseAppRoute(); const NeonBaseClientRoute();
@override @override
Page<void> buildPage(final BuildContext context, final GoRouterState state) => NoTransitionPage( Page<void> buildPage(final BuildContext context, final GoRouterState state) => NoTransitionPage(
@ -16,5 +16,5 @@ abstract class NeonBaseAppRoute extends GoRouteData {
); );
} }
/// Prefix for [NeonBaseAppRoute]s. /// Prefix for [NeonBaseClientRoute]s.
const appsBaseRoutePrefix = '/apps/'; 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 { class GlobalOptions extends OptionsCollection {
GlobalOptions( GlobalOptions(
this._packageInfo, this._packageInfo,
) : super(const AppStorage(StorageKeys.global)) { ) : super(const ClientStorage(StorageKeys.global)) {
pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener); pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener);
rememberLastUsedAccount.addListener(_rememberLastUsedAccountListener); rememberLastUsedAccount.addListener(_rememberLastUsedAccountListener);
} }

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

@ -25,7 +25,7 @@ class PushUtils {
const PushUtils._(); const PushUtils._();
static notifications.RSAKeypair loadRSAKeypair() { static notifications.RSAKeypair loadRSAKeypair() {
const storage = AppStorage(StorageKeys.notifications); const storage = ClientStorage(StorageKeys.notifications);
const keyDevicePrivateKey = 'device-private-key'; const keyDevicePrivateKey = 'device-private-key';
final notifications.RSAKeypair keypair; final notifications.RSAKeypair keypair;
@ -132,11 +132,11 @@ class PushUtils {
} }
if (notification?.shouldNotify ?? true) { if (notification?.shouldNotify ?? true) {
final appID = notification?.app ?? pushNotification.subject.app ?? 'nextcloud'; final clientID = notification?.app ?? pushNotification.subject.app ?? 'nextcloud';
String? appName = localizations.appImplementationName(appID); String? clientName = localizations.clientImplementationName(clientID);
if (appName.isEmpty) { if (clientName.isEmpty) {
debugPrint('Missing app name for $appID'); debugPrint('Missing client name for $clientID');
appName = null; clientName = null;
} }
final title = (notification?.subject ?? pushNotification.subject.subject)!; final title = (notification?.subject ?? pushNotification.subject.subject)!;
final message = (notification?.message.isNotEmpty ?? false) ? notification!.message : null; final message = (notification?.message.isNotEmpty ?? false) ? notification!.message : null;
@ -144,14 +144,14 @@ class PushUtils {
await localNotificationsPlugin.show( await localNotificationsPlugin.show(
_getNotificationID(instance, pushNotification), _getNotificationID(instance, pushNotification),
message != null && appName != null ? '$appName: $title' : title, message != null && clientName != null ? '$clientName: $title' : title,
message, message,
NotificationDetails( NotificationDetails(
android: AndroidNotificationDetails( android: AndroidNotificationDetails(
appID, clientID,
appName ?? appID, clientName ?? clientID,
subText: accounts.length > 1 && account != null ? account.humanReadableID : null, subText: accounts.length > 1 && account != null ? account.humanReadableID : null,
groupKey: 'app_$appID', groupKey: 'app_$clientID',
icon: '@mipmap/ic_launcher', icon: '@mipmap/ic_launcher',
largeIcon: largeIconBitmap, largeIcon: largeIconBitmap,
when: when?.millisecondsSinceEpoch, 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/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart'; import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.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/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/notifications_interface.dart';
import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/widgets/account_switcher_button.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/error.dart';
import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart';
import 'package:provider/provider.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 AccountsBloc accountsBloc = NeonProvider.of<AccountsBloc>(context);
late final accounts = accountsBloc.accounts.value; late final accounts = accountsBloc.accounts.value;
late final account = accountsBloc.activeAccount.value!; late final account = accountsBloc.activeAccount.value!;
late final appsBloc = accountsBloc.activeAppsBloc; late final clientsBloc = accountsBloc.activeClientsBloc;
late final unifiedSearchBloc = accountsBloc.activeUnifiedSearchBloc; late final unifiedSearchBloc = accountsBloc.activeUnifiedSearchBloc;
final _searchBarFocusNode = FocusNode(); final _searchBarFocusNode = FocusNode();
@ -62,11 +62,11 @@ class _NeonAppBarState extends State<NeonAppBar> {
} }
@override @override
Widget build(final BuildContext context) => ResultBuilder<Iterable<AppImplementation>>.behaviorSubject( Widget build(final BuildContext context) => ResultBuilder<Iterable<ClientImplementation>>.behaviorSubject(
subject: appsBloc.appImplementations, subject: clientsBloc.clientImplementations,
builder: (final context, final appImplementations) => StreamBuilder( builder: (final context, final clientImplementations) => StreamBuilder(
stream: appsBloc.activeApp, stream: clientsBloc.activeClient,
builder: (final context, final activeAppSnapshot) => StreamBuilder( builder: (final context, final activeClientSnapshot) => StreamBuilder(
stream: unifiedSearchBloc.enabled, stream: unifiedSearchBloc.enabled,
builder: (final context, final unifiedSearchEnabledSnapshot) { builder: (final context, final unifiedSearchEnabledSnapshot) {
final unifiedSearchEnabled = unifiedSearchEnabledSnapshot.data ?? false; final unifiedSearchEnabled = unifiedSearchEnabledSnapshot.data ?? false;
@ -78,24 +78,24 @@ class _NeonAppBarState extends State<NeonAppBar> {
children: [ children: [
Row( Row(
children: [ children: [
if (activeAppSnapshot.hasData) ...[ if (activeClientSnapshot.hasData) ...[
Flexible( Flexible(
child: Text( child: Text(
activeAppSnapshot.requireData.name(context), activeClientSnapshot.requireData.name(context),
), ),
), ),
], ],
if (appImplementations.hasError) ...[ if (clientImplementations.hasError) ...[
const SizedBox( const SizedBox(
width: 8, width: 8,
), ),
NeonError( NeonError(
appImplementations.error, clientImplementations.error,
onRetry: appsBloc.refresh, onRetry: clientsBloc.refresh,
onlyIcon: true, onlyIcon: true,
), ),
], ],
if (appImplementations.isLoading) ...[ if (clientImplementations.isLoading) ...[
const SizedBox( const SizedBox(
width: 8, width: 8,
), ),
@ -179,7 +179,7 @@ class NotificationIconButton extends StatefulWidget {
class _NotificationIconButtonState extends State<NotificationIconButton> { class _NotificationIconButtonState extends State<NotificationIconButton> {
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc; late ClientsBloc _clientsBloc;
late List<Account> _accounts; late List<Account> _accounts;
late Account _account; late Account _account;
late StreamSubscription<void> notificationSubscription; late StreamSubscription<void> notificationSubscription;
@ -188,14 +188,14 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
void initState() { void initState() {
super.initState(); super.initState();
_accountsBloc = NeonProvider.of<AccountsBloc>(context); _accountsBloc = NeonProvider.of<AccountsBloc>(context);
_appsBloc = _accountsBloc.activeAppsBloc; _clientsBloc = _accountsBloc.activeClientsBloc;
_accounts = _accountsBloc.accounts.value; _accounts = _accountsBloc.accounts.value;
_account = _accountsBloc.activeAccount.value!; _account = _accountsBloc.activeAccount.value!;
notificationSubscription = _appsBloc.openNotifications.listen((final _) async { notificationSubscription = _clientsBloc.openNotifications.listen((final _) async {
final notificationsAppImplementation = _appsBloc.notificationsAppImplementation.valueOrNull; final notificationsClientImplementation = _clientsBloc.notificationsClientImplementation.valueOrNull;
if (notificationsAppImplementation != null && notificationsAppImplementation.hasData) { if (notificationsClientImplementation != null && notificationsClientImplementation.hasData) {
await _openNotifications(notificationsAppImplementation.data!); await _openNotifications(notificationsClientImplementation.data!);
} }
}); });
} }
@ -209,7 +209,7 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
// TODO: migrate to go_router with a separate page // TODO: migrate to go_router with a separate page
Future<void> _openNotifications( Future<void> _openNotifications(
final NotificationsAppInterface app, final NotificationsClientInterface client,
) async { ) async {
final page = Scaffold( final page = Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
@ -217,7 +217,7 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(app.name(context)), Text(client.name(context)),
if (_accounts.length > 1) ...[ if (_accounts.length > 1) ...[
Text( Text(
_account.humanReadableID, _account.humanReadableID,
@ -227,13 +227,13 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
], ],
), ),
), ),
body: app.page, body: client.page,
); );
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (final context) => Provider<NotificationsBlocInterface>( builder: (final context) => Provider<NotificationsBlocInterface>(
create: (final context) => app.getBloc(_account), create: (final context) => client.getBloc(_account),
child: page, child: page,
), ),
), ),
@ -241,14 +241,14 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
} }
@override @override
Widget build(final BuildContext context) => ResultBuilder<NotificationsAppInterface?>.behaviorSubject( Widget build(final BuildContext context) => ResultBuilder<NotificationsClientInterface?>.behaviorSubject(
subject: _appsBloc.notificationsAppImplementation, subject: _clientsBloc.notificationsClientImplementation,
builder: (final context, final notificationsAppImplementation) { builder: (final context, final notificationsClientImplementation) {
if (!notificationsAppImplementation.hasData) { if (!notificationsClientImplementation.hasData) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final notificationsImplementationData = notificationsAppImplementation.data!; final notificationsImplementationData = notificationsClientImplementation.data!;
final notificationBloc = notificationsImplementationData.getBloc(_account); final notificationBloc = notificationsImplementationData.getBloc(_account);
return IconButton( return IconButton(
@ -256,11 +256,11 @@ class _NotificationIconButtonState extends State<NotificationIconButton> {
onPressed: () async { onPressed: () async {
await _openNotifications(notificationsImplementationData); await _openNotifications(notificationsImplementationData);
}, },
tooltip: NeonLocalizations.of(context).appImplementationName(notificationsImplementationData.id), tooltip: NeonLocalizations.of(context).clientImplementationName(notificationsImplementationData.id),
icon: StreamBuilder<int>( icon: StreamBuilder<int>(
stream: notificationsImplementationData.getUnreadCounter(notificationBloc), stream: notificationsImplementationData.getUnreadCounter(notificationBloc),
builder: (final context, final unreadCounterSnapshot) => NeonAppImplementationIcon( builder: (final context, final unreadCounterSnapshot) => NeonClientImplementationIcon(
appImplementation: notificationsImplementationData, clientImplementation: notificationsImplementationData,
unreadCount: unreadCounterSnapshot.data, 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:flutter/material.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/client_implementation.dart';
@internal @internal
class NeonAppImplementationIcon extends StatelessWidget { class NeonClientImplementationIcon extends StatelessWidget {
const NeonAppImplementationIcon({ const NeonClientImplementationIcon({
required this.appImplementation, required this.clientImplementation,
this.unreadCount, this.unreadCount,
this.color, this.color,
this.size, this.size,
super.key, super.key,
}); });
final AppImplementation appImplementation; final ClientImplementation clientImplementation;
final int? unreadCount; final int? unreadCount;
@ -29,7 +29,7 @@ class NeonAppImplementationIcon extends StatelessWidget {
final icon = Container( final icon = Container(
margin: const EdgeInsets.all(5), margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon( child: clientImplementation.buildIcon(
size: size, size: size,
color: color, 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/l10n/localizations.dart';
import 'package:neon/src/bloc/result.dart'; import 'package:neon/src/bloc/result.dart';
import 'package:neon/src/blocs/accounts.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/app_implementation.dart'; import 'package:neon/src/models/client_implementation.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/provider.dart';
import 'package:neon/src/widgets/drawer_destination.dart'; import 'package:neon/src/widgets/drawer_destination.dart';
@ -24,17 +24,17 @@ class NeonDrawer extends StatelessWidget {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final accountsBloc = NeonProvider.of<AccountsBloc>(context); final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appsBloc = accountsBloc.activeAppsBloc; final clientsBloc = accountsBloc.activeClientsBloc;
return ResultBuilder.behaviorSubject( return ResultBuilder.behaviorSubject(
subject: appsBloc.appImplementations, subject: clientsBloc.clientImplementations,
builder: (final context, final snapshot) { builder: (final context, final snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return _NeonDrawer( return _NeonDrawer(
apps: snapshot.requireData, clients: snapshot.requireData,
); );
}, },
); );
@ -43,10 +43,10 @@ class NeonDrawer extends StatelessWidget {
class _NeonDrawer extends StatefulWidget { class _NeonDrawer extends StatefulWidget {
const _NeonDrawer({ const _NeonDrawer({
required this.apps, required this.clients,
}); });
final Iterable<AppImplementation> apps; final Iterable<ClientImplementation> clients;
@override @override
State<_NeonDrawer> createState() => __NeonDrawerState(); State<_NeonDrawer> createState() => __NeonDrawerState();
@ -54,53 +54,53 @@ class _NeonDrawer extends StatefulWidget {
class __NeonDrawerState extends State<_NeonDrawer> { class __NeonDrawerState extends State<_NeonDrawer> {
late AccountsBloc _accountsBloc; late AccountsBloc _accountsBloc;
late AppsBloc _appsBloc; late ClientsBloc _clientsBloc;
late List<AppImplementation> _apps; late List<ClientImplementation> _clients;
late int _activeApp; late int _activeClient;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_accountsBloc = NeonProvider.of<AccountsBloc>(context); _accountsBloc = NeonProvider.of<AccountsBloc>(context);
_appsBloc = _accountsBloc.activeAppsBloc; _clientsBloc = _accountsBloc.activeClientsBloc;
_apps = widget.apps.toList(); _clients = widget.clients.toList();
_activeApp = _apps.indexWhere((final app) => app.id == _appsBloc.activeApp.valueOrNull?.id); _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(); Scaffold.maybeOf(context)?.closeDrawer();
// selected item is not a registered app like the SettingsPage // selected item is not a registered client like the SettingsPage
if (index >= _apps.length) { if (index >= _clients.length) {
const SettingsRoute().go(context); const SettingsRoute().go(context);
return; return;
} }
setState(() { setState(() {
_activeApp = index; _activeClient = index;
}); });
unawaited(_appsBloc.setActiveApp(_apps[index].id)); unawaited(_clientsBloc.setActiveClient(_clients[index].id));
//context.goNamed(apps[index].routeName); //context.goNamed(apps[index].routeName);
} }
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final appDestinations = _apps.map( final clientDestinations = _clients.map(
(final app) => NavigationDrawerDestinationExtension.fromNeonDestination( (final client) => NavigationDrawerDestinationExtension.fromNeonDestination(
app.destination(context), client.destination(context),
), ),
); );
final drawer = NavigationDrawer( final drawer = NavigationDrawer(
selectedIndex: _activeApp, selectedIndex: _activeClient,
onDestinationSelected: onAppChange, onDestinationSelected: onDestinationSelected,
children: [ children: [
const NeonDrawerHeader(), const NeonDrawerHeader(),
...appDestinations, ...clientDestinations,
NavigationDrawerDestination( NavigationDrawerDestination(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
label: Text(NeonLocalizations.of(context).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/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/confirmation_dialog.dart';
export 'package:neon/src/utils/exceptions.dart'; export 'package:neon/src/utils/exceptions.dart';
export 'package:neon/src/utils/hex_color.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 // ignore: missing_override_of_must_be_overridden
class OptionMock extends Mock implements ToggleOption {} class OptionMock extends Mock implements ToggleOption {}
class Collection extends NextcloudAppOptions { class Collection extends NextcloudClientOptions {
Collection(final List<Option<Object>> options) : super(const AppStorage(StorageKeys.apps)) { Collection(final List<Option<Object>> options) : super(const ClientStorage(StorageKeys.clients)) {
super.options = options; 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:mocktail/mocktail.dart';
import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.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/exportable.dart';
import 'package:neon/src/settings/models/options_collection.dart'; import 'package:neon/src/settings/models/options_collection.dart';
import 'package:neon/src/settings/utils/settings_export_helper.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'; import 'package:test/test.dart';
// ignore: missing_override_of_must_be_overridden, avoid_implementing_value_types // 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 {} class AccountsBlocMock extends Mock implements AccountsBloc {}
@ -29,14 +29,14 @@ class ExporterMock extends Mock implements Exportable {}
void main() { void main() {
group('Exporter', () { group('Exporter', () {
test('AccountsBlocExporter', () { test('AccountsBlocExporter', () {
var exporter = const AppImplementationsExporter([]); var exporter = const ClientImplementationsExporter([]);
var export = exporter.export(); var export = exporter.export();
expect(Map.fromEntries([export]), {'app': <String, dynamic>{}}); expect(Map.fromEntries([export]), {'app': <String, dynamic>{}});
final fakeApp = FakeAppImplementation(); final fakeApp = FakeAppImplementation();
final fakeOptions = NextcloudAppOptionsMock(); final fakeOptions = NextcloudAppOptionsMock();
exporter = AppImplementationsExporter([fakeApp]); exporter = ClientImplementationsExporter([fakeApp]);
const appValue = MapEntry('appID', 'value'); const appValue = MapEntry('appID', 'value');
const appExport = { const appExport = {

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

@ -17,12 +17,12 @@ void main() {
group('AppStorage', () { group('AppStorage', () {
test('formatKey', () async { test('formatKey', () async {
var appStorage = const AppStorage(StorageKeys.accounts); var appStorage = const ClientStorage(StorageKeys.accounts);
var key = appStorage.formatKey('test-key'); var key = appStorage.formatKey('test-key');
expect(key, 'accounts-test-key'); expect(key, 'accounts-test-key');
expect(appStorage.id, StorageKeys.accounts.value); 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'); key = appStorage.formatKey('test-key');
expect(key, 'accounts-test-suffix-test-key'); expect(key, 'accounts-test-suffix-test-key');
expect(appStorage.id, 'test-suffix'); expect(appStorage.id, 'test-suffix');
@ -31,7 +31,7 @@ void main() {
test('interface', () async { test('interface', () async {
final sharedPreferences = SharedPreferencesMock(); final sharedPreferences = SharedPreferencesMock();
NeonStorage.mock(sharedPreferences); NeonStorage.mock(sharedPreferences);
const appStorage = AppStorage(StorageKeys.accounts); const appStorage = ClientStorage(StorageKeys.accounts);
const key = 'key'; const key = 'key';
final formattedKey = appStorage.formatKey(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'; import 'package:nextcloud/nextcloud.dart';
/// Implementation of the server `dashboard` app. /// Implementation of the server `dashboard` app.
class DashboardApp extends AppImplementation<DashboardBloc, DashboardAppSpecificOptions> { class DashboardClient extends ClientImplementation<DashboardBloc, DashboardClientSpecificOptions> {
/// Creates a new Dashboard app implementation instance. /// Creates a new Dashboard client implementation instance.
DashboardApp(); DashboardClient();
@override @override
final String id = AppIDs.dashboard; final String id = AppIDs.dashboard;
@ -24,7 +24,7 @@ class DashboardApp extends AppImplementation<DashboardBloc, DashboardAppSpecific
final List<Locale> supportedLocales = DashboardLocalizations.supportedLocales; final List<Locale> supportedLocales = DashboardLocalizations.supportedLocales;
@override @override
late final DashboardAppSpecificOptions options = DashboardAppSpecificOptions(storage); late final DashboardClientSpecificOptions options = DashboardClientSpecificOptions(storage);
@override @override
DashboardBloc buildBloc(final Account account) => DashboardBloc(account); DashboardBloc buildBloc(final Account account) => DashboardBloc(account);
@ -33,7 +33,7 @@ class DashboardApp extends AppImplementation<DashboardBloc, DashboardAppSpecific
final Widget page = const DashboardMainPage(); final Widget page = const DashboardMainPage();
@override @override
final RouteBase route = $dashboardAppRoute; final RouteBase route = $dashboardClientRoute;
@override @override
(bool?, String?) isSupported( (bool?, String?) isSupported(

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

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

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

@ -6,15 +6,15 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart'; part 'routes.g.dart';
/// Route for the dashboard app. /// Route for the dashboard client.
@TypedGoRoute<DashboardAppRoute>( @TypedGoRoute<DashboardClientRoute>(
path: '$appsBaseRoutePrefix${AppIDs.dashboard}', path: '$clientsBaseRoutePrefix${AppIDs.dashboard}',
name: AppIDs.dashboard, name: AppIDs.dashboard,
) )
@immutable @immutable
class DashboardAppRoute extends NeonBaseAppRoute { class DashboardClientRoute extends NeonBaseClientRoute {
/// Creates a new dashboard app route. /// Creates a new dashboard client route.
const DashboardAppRoute(); const DashboardClientRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const DashboardMainPage(); 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 => [ List<RouteBase> get $appRoutes => [
$dashboardAppRoute, $dashboardClientRoute,
]; ];
RouteBase get $dashboardAppRoute => GoRouteData.$route( RouteBase get $dashboardClientRoute => GoRouteData.$route(
path: '/apps/dashboard', path: '/apps/dashboard',
name: 'dashboard', name: 'dashboard',
factory: $DashboardAppRouteExtension._fromState, factory: $DashboardClientRouteExtension._fromState,
); );
extension $DashboardAppRouteExtension on DashboardAppRoute { extension $DashboardClientRouteExtension on DashboardClientRoute {
static DashboardAppRoute _fromState(GoRouterState state) => const DashboardAppRoute(); static DashboardClientRoute _fromState(GoRouterState state) => const DashboardClientRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/apps/dashboard', '/apps/dashboard',

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

@ -25,7 +25,7 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
unawaited(refresh()); unawaited(refresh());
} }
final FilesAppSpecificOptions options; final FilesClientSpecificOptions options;
final Account account; final Account account;
@override @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); options.downloadQueueParallelism.addListener(_downloadParallelismListener);
} }
final FilesAppSpecificOptions options; final FilesClientSpecificOptions options;
final Account account; final Account account;
late final browser = getNewFilesBrowserBloc(); 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/file_preview.dart';
part 'widgets/navigator.dart'; part 'widgets/navigator.dart';
class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> { class FilesClient extends ClientImplementation<FilesBloc, FilesClientSpecificOptions> {
FilesApp(); FilesClient();
@override @override
final String id = AppIDs.files; final String id = AppIDs.files;
@ -59,7 +59,7 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
final List<Locale> supportedLocales = FilesLocalizations.supportedLocales; final List<Locale> supportedLocales = FilesLocalizations.supportedLocales;
@override @override
late final FilesAppSpecificOptions options = FilesAppSpecificOptions(storage); late final FilesClientSpecificOptions options = FilesClientSpecificOptions(storage);
@override @override
FilesBloc buildBloc(final Account account) => FilesBloc( FilesBloc buildBloc(final Account account) => FilesBloc(
@ -71,7 +71,7 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
final Widget page = const FilesMainPage(); final Widget page = const FilesMainPage();
@override @override
final RouteBase route = $filesAppRoute; final RouteBase route = $filesClientRoute;
@override @override
(bool? supported, String? minimumVersion) isSupported( (bool? supported, String? minimumVersion) isSupported(

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

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

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

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart'; part 'routes.g.dart';
@TypedGoRoute<FilesAppRoute>( @TypedGoRoute<FilesClientRoute>(
path: '$appsBaseRoutePrefix${AppIDs.files}', path: '$clientsBaseRoutePrefix${AppIDs.files}',
name: AppIDs.files, name: AppIDs.files,
) )
@immutable @immutable
class FilesAppRoute extends NeonBaseAppRoute { class FilesClientRoute extends NeonBaseClientRoute {
const FilesAppRoute(); const FilesClientRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const FilesMainPage(); 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 => [ List<RouteBase> get $appRoutes => [
$filesAppRoute, $filesClientRoute,
]; ];
RouteBase get $filesAppRoute => GoRouteData.$route( RouteBase get $filesClientRoute => GoRouteData.$route(
path: '/apps/files', path: '/apps/files',
name: 'files', name: 'files',
factory: $FilesAppRouteExtension._fromState, factory: $FilesClientRouteExtension._fromState,
); );
extension $FilesAppRouteExtension on FilesAppRoute { extension $FilesClientRouteExtension on FilesClientRoute {
static FilesAppRoute _fromState(GoRouterState state) => const FilesAppRoute(); static FilesClientRoute _fromState(GoRouterState state) => const FilesClientRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/apps/files', '/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 NewsBloc _newsBloc;
final NewsAppSpecificOptions options; final NewsClientSpecificOptions options;
final Account account; final Account account;
final int? id; final int? id;
final ListType? listType; final ListType? listType;

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

@ -47,7 +47,7 @@ class NewsBloc extends InteractiveBloc implements NewsBlocEvents, NewsBlocStates
@override @override
NewsBloc get _newsBloc => this; NewsBloc get _newsBloc => this;
@override @override
final NewsAppSpecificOptions options; final NewsClientSpecificOptions options;
@override @override
@override @override
final Account account; 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/folder_view.dart';
part 'widgets/folders_view.dart'; part 'widgets/folders_view.dart';
class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> { class NewsClient extends ClientImplementation<NewsBloc, NewsClientSpecificOptions> {
NewsApp(); NewsClient();
@override @override
final String id = AppIDs.news; final String id = AppIDs.news;
@ -67,7 +67,7 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
final List<Locale> supportedLocales = NewsLocalizations.supportedLocales; final List<Locale> supportedLocales = NewsLocalizations.supportedLocales;
@override @override
late final NewsAppSpecificOptions options = NewsAppSpecificOptions(storage); late final NewsClientSpecificOptions options = NewsClientSpecificOptions(storage);
@override @override
NewsBloc buildBloc(final Account account) => NewsBloc( NewsBloc buildBloc(final Account account) => NewsBloc(
@ -79,7 +79,7 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
final Widget page = const NewsMainPage(); final Widget page = const NewsMainPage();
@override @override
final RouteBase route = $newsAppRoute; final RouteBase route = $newsClientRoute;
@override @override
BehaviorSubject<int> getUnreadCounter(final NewsBloc bloc) => bloc.unreadCounter; 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'; part of 'neon_news.dart';
class NewsAppSpecificOptions extends NextcloudAppOptions { class NewsClientSpecificOptions extends NextcloudClientOptions {
NewsAppSpecificOptions(super.storage) { NewsClientSpecificOptions(super.storage) {
super.categories = [ super.categories = [
generalCategory, generalCategory,
articlesCategory, articlesCategory,

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

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart'; part 'routes.g.dart';
@TypedGoRoute<NewsAppRoute>( @TypedGoRoute<NewsClientRoute>(
path: '$appsBaseRoutePrefix${AppIDs.news}', path: '$clientsBaseRoutePrefix${AppIDs.news}',
name: AppIDs.news, name: AppIDs.news,
) )
@immutable @immutable
class NewsAppRoute extends NeonBaseAppRoute { class NewsClientRoute extends NeonBaseClientRoute {
const NewsAppRoute(); const NewsClientRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const NewsMainPage(); 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 => [ List<RouteBase> get $appRoutes => [
$newsAppRoute, $newsClientRoute,
]; ];
RouteBase get $newsAppRoute => GoRouteData.$route( RouteBase get $newsClientRoute => GoRouteData.$route(
path: '/apps/news', path: '/apps/news',
name: 'news', name: 'news',
factory: $NewsAppRouteExtension._fromState, factory: $NewsClientRouteExtension._fromState,
); );
extension $NewsAppRouteExtension on NewsAppRoute { extension $NewsClientRouteExtension on NewsClientRoute {
static NewsAppRoute _fromState(GoRouterState state) => const NewsAppRoute(); static NewsClientRoute _fromState(GoRouterState state) => const NewsClientRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/apps/news', '/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 NotesBloc _notesBloc;
final _updateQueue = Queue(); 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()); unawaited(refresh());
} }
final NotesAppSpecificOptions options; final NotesClientSpecificOptions options;
final Account account; final Account account;
@override @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_floating_action_button.dart';
part 'widgets/notes_view.dart'; part 'widgets/notes_view.dart';
class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> { class NotesClient extends ClientImplementation<NotesBloc, NotesClientSpecificOptions> {
NotesApp(); NotesClient();
@override @override
final String id = AppIDs.notes; final String id = AppIDs.notes;
@ -56,7 +56,7 @@ class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
final LocalizationsDelegate<NotesLocalizations> localizationsDelegate = NotesLocalizations.delegate; final LocalizationsDelegate<NotesLocalizations> localizationsDelegate = NotesLocalizations.delegate;
@override @override
late final NotesAppSpecificOptions options = NotesAppSpecificOptions(storage); late final NotesClientSpecificOptions options = NotesClientSpecificOptions(storage);
@override @override
NotesBloc buildBloc(final Account account) => NotesBloc( NotesBloc buildBloc(final Account account) => NotesBloc(
@ -68,7 +68,7 @@ class NotesApp extends AppImplementation<NotesBloc, NotesAppSpecificOptions> {
final Widget page = const NotesMainPage(); final Widget page = const NotesMainPage();
@override @override
final RouteBase route = $notesAppRoute; final RouteBase route = $notesClientRoute;
@override @override
(bool? supported, String? minimumVersion) isSupported( (bool? supported, String? minimumVersion) isSupported(

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

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

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

@ -6,13 +6,13 @@ import 'package:nextcloud/nextcloud.dart';
part 'routes.g.dart'; part 'routes.g.dart';
@TypedGoRoute<NotesAppRoute>( @TypedGoRoute<NotesClientRoute>(
path: '$appsBaseRoutePrefix${AppIDs.notes}', path: '$clientsBaseRoutePrefix${AppIDs.notes}',
name: AppIDs.notes, name: AppIDs.notes,
) )
@immutable @immutable
class NotesAppRoute extends NeonBaseAppRoute { class NotesClientRoute extends NeonBaseClientRoute {
const NotesAppRoute(); const NotesClientRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const NotesMainPage(); 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 => [ List<RouteBase> get $appRoutes => [
$notesAppRoute, $notesClientRoute,
]; ];
RouteBase get $notesAppRoute => GoRouteData.$route( RouteBase get $notesClientRoute => GoRouteData.$route(
path: '/apps/notes', path: '/apps/notes',
name: 'notes', name: 'notes',
factory: $NotesAppRouteExtension._fromState, factory: $NotesClientRouteExtension._fromState,
); );
extension $NotesAppRouteExtension on NotesAppRoute { extension $NotesClientRouteExtension on NotesClientRoute {
static NotesAppRoute _fromState(GoRouterState state) => const NotesAppRoute(); static NotesClientRoute _fromState(GoRouterState state) => const NotesClientRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/apps/notes', '/apps/notes',

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

@ -29,7 +29,7 @@ class NotificationsBloc extends InteractiveBloc
} }
@override @override
final NotificationsAppSpecificOptions options; final NotificationsClientSpecificOptions options;
final Account _account; final Account _account;
late final NeonTimer _timer; 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 'options.dart';
part 'pages/main.dart'; part 'pages/main.dart';
class NotificationsApp extends AppImplementation<NotificationsBloc, NotificationsAppSpecificOptions> class NotificationsClient extends ClientImplementation<NotificationsBloc, NotificationsClientSpecificOptions>
implements implements
// ignore: avoid_implementing_value_types // ignore: avoid_implementing_value_types
NotificationsAppInterface<NotificationsBloc, NotificationsAppSpecificOptions> { NotificationsClientInterface<NotificationsBloc, NotificationsClientSpecificOptions> {
NotificationsApp(); NotificationsClient();
@override @override
final String id = AppIDs.notifications; final String id = AppIDs.notifications;
@ -38,7 +38,7 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
final List<Locale> supportedLocales = NotificationsLocalizations.supportedLocales; final List<Locale> supportedLocales = NotificationsLocalizations.supportedLocales;
@override @override
late final NotificationsAppSpecificOptions options = NotificationsAppSpecificOptions(storage); late final NotificationsClientSpecificOptions options = NotificationsClientSpecificOptions(storage);
@override @override
NotificationsBloc buildBloc(final Account account) => NotificationsBloc( NotificationsBloc buildBloc(final Account account) => NotificationsBloc(
@ -50,7 +50,7 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
final Widget page = const NotificationsMainPage(); final Widget page = const NotificationsMainPage();
@override @override
final RouteBase route = $notificationsAppRoute; final RouteBase route = $notificationsClientRoute;
@override @override
BehaviorSubject<int> getUnreadCounter(final NotificationsBloc bloc) => bloc.unreadCounter; 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'; part of 'neon_notifications.dart';
class NotificationsAppSpecificOptions extends NextcloudAppOptions implements NotificationsOptionsInterface { class NotificationsClientSpecificOptions extends NextcloudClientOptions implements NotificationsOptionsInterface {
NotificationsAppSpecificOptions(super.storage) { NotificationsClientSpecificOptions(super.storage) {
super.categories = []; super.categories = [];
super.options = []; super.options = [];
} }

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

@ -54,7 +54,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
final BuildContext context, final BuildContext context,
final notifications.Notification notification, 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( return ListTile(
title: Text(notification.subject), title: Text(notification.subject),
@ -75,8 +75,8 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
), ),
], ],
), ),
leading: app != null leading: client != null
? app.buildIcon( ? client.buildIcon(
size: largeIconSize, size: largeIconSize,
) )
: SizedBox.fromSize( : SizedBox.fromSize(
@ -91,10 +91,10 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
if (notification.app == AppIDs.notifications) { if (notification.app == AppIDs.notifications) {
return; return;
} }
if (app != null) { if (client != null) {
// TODO: use go_router once implemented // TODO: use go_router once implemented
final accountsBloc = NeonProvider.of<AccountsBloc>(context); final accountsBloc = NeonProvider.of<AccountsBloc>(context);
await accountsBloc.activeAppsBloc.setActiveApp(app.id); await accountsBloc.activeClientsBloc.setActiveClient(client.id);
} else { } else {
final colorScheme = Theme.of(context).colorScheme; 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'; part 'routes.g.dart';
@TypedGoRoute<NotificationsAppRoute>( @TypedGoRoute<NotificationsClientRoute>(
path: '$appsBaseRoutePrefix${AppIDs.notifications}', path: '$clientsBaseRoutePrefix${AppIDs.notifications}',
name: AppIDs.notifications, name: AppIDs.notifications,
) )
@immutable @immutable
class NotificationsAppRoute extends NeonBaseAppRoute { class NotificationsClientRoute extends NeonBaseClientRoute {
const NotificationsAppRoute(); const NotificationsClientRoute();
@override @override
Widget build(final BuildContext context, final GoRouterState state) => const NotificationsMainPage(); 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 => [ List<RouteBase> get $appRoutes => [
$notificationsAppRoute, $notificationsClientRoute,
]; ];
RouteBase get $notificationsAppRoute => GoRouteData.$route( RouteBase get $notificationsClientRoute => GoRouteData.$route(
path: '/apps/notifications', path: '/apps/notifications',
name: 'notifications', name: 'notifications',
factory: $NotificationsAppRouteExtension._fromState, factory: $NotificationsClientRouteExtension._fromState,
); );
extension $NotificationsAppRouteExtension on NotificationsAppRoute { extension $NotificationsClientRouteExtension on NotificationsClientRoute {
static NotificationsAppRoute _fromState(GoRouterState state) => const NotificationsAppRoute(); static NotificationsClientRoute _fromState(GoRouterState state) => const NotificationsClientRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/apps/notifications', '/apps/notifications',

Loading…
Cancel
Save