diff --git a/packages/neon/neon/lib/src/models/account.dart b/packages/neon/neon/lib/src/models/account.dart index ddc11385..72c22e56 100644 --- a/packages/neon/neon/lib/src/models/account.dart +++ b/packages/neon/neon/lib/src/models/account.dart @@ -22,9 +22,11 @@ abstract interface class Credentials { abstract final String? password; } +/// Account data. @JsonSerializable() @immutable class Account implements Credentials { + /// Creates a new account. Account({ required this.serverURL, required this.username, @@ -39,8 +41,10 @@ class Account implements Credentials { cookieJar: CookieJar(), ); + /// Creates a new account object from the given [json] data. factory Account.fromJson(final Map json) => _$AccountFromJson(json); + /// Parses this object into a json like map. Map toJson() => _$AccountToJson(this); @override @@ -49,6 +53,8 @@ class Account implements Credentials { final String username; @override final String? password; + + /// The user agent to use. final String? userAgent; @override @@ -62,8 +68,13 @@ class Account implements Credentials { @override int get hashCode => serverURL.hashCode + username.hashCode; + /// An authenticated API client. final NextcloudClient client; + /// The unique ID of the account. + /// + /// Implemented in a primitive way hashing the [username] and [serverURL]. + /// IDs are globally cached in [_idCache]. String get id { final key = '$username@$serverURL'; @@ -108,10 +119,20 @@ class Account implements Credentials { Uri stripUri(final Uri uri) => Uri.parse(uri.toString().replaceFirst(serverURL.toString(), '')); } +/// Global [Account.id] cache. Map _idCache = {}; +/// Extension to find an account by id in a Iterable. extension AccountFind on Iterable { + /// Returns the first [Account] matching [accountID] by [Account.id]. + /// + /// If no `Account` was found `null` is returned. Account? tryFind(final String? accountID) => firstWhereOrNull((final account) => account.id == accountID); + + /// Returns the first [Account] matching [accountID] by [Account.id]. + /// + /// Throws a [StateError] if no `Account` was found. + /// Use [tryFind] to get a nullable result. Account find(final String accountID) => firstWhere((final account) => account.id == accountID); } diff --git a/packages/neon/neon/lib/src/models/account_cache.dart b/packages/neon/neon/lib/src/models/account_cache.dart index 967e5234..d075eed5 100644 --- a/packages/neon/neon/lib/src/models/account_cache.dart +++ b/packages/neon/neon/lib/src/models/account_cache.dart @@ -3,6 +3,7 @@ import 'package:neon/src/models/disposable.dart'; /// Cache for [Account] specific [Disposable] objects. class AccountCache implements Disposable { + /// Creates a new account cache. AccountCache(); final Map _cache = {}; diff --git a/packages/neon/neon/lib/src/models/app_implementation.dart b/packages/neon/neon/lib/src/models/app_implementation.dart index e57aa2bd..9ba66fd7 100644 --- a/packages/neon/neon/lib/src/models/app_implementation.dart +++ b/packages/neon/neon/lib/src/models/app_implementation.dart @@ -20,18 +20,39 @@ import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; import 'package:vector_graphics/vector_graphics.dart'; +/// Base implementation of a Neon app. +/// +/// It is mandatory to provide a precompiled SVG under `assets/app.svg.vec`. +/// SVGs can be precompiled with `https://pub.dev/packages/vector_graphics_compiler` @immutable abstract class AppImplementation implements Disposable { + /// The unique id of an app. + /// + /// It is common to specify them in `AppIDs`. String get id; + + /// {@macro flutter.widgets.widgetsApp.localizationsDelegates} LocalizationsDelegate get localizationsDelegate; + + /// {@macro flutter.widgets.widgetsApp.supportedLocales} Iterable get supportedLocales; + /// Default localized app name used in [name]. + /// + /// Defaults to the frameworks mapping of the [id] to a localized name. String nameFromLocalization(final NeonLocalizations localizations) => localizations.appImplementationName(id); + + /// Localized name of this app. String name(final BuildContext context) => nameFromLocalization(NeonLocalizations.of(context)); + /// The [SettingsStorage] for this app. @protected late final AppStorage storage = AppStorage(StorageKeys.apps, id); + /// The options associated with this app. + /// + /// Options will be added to the settings page providing a global place to + /// adjust the behavior of an app. @mustBeOverridden R get options; @@ -46,13 +67,25 @@ abstract class AppImplementation ) => null; + /// Cache for all blocs. + /// + /// To access a bloc use [getBloc] instead. final blocsCache = AccountCache(); + /// Returns a bloc [T] from the [blocsCache] or builds a new one if absent. T getBloc(final Account account) => blocsCache[account] ??= buildBloc(account); + /// Build the bloc [T] for the given [account]. + /// + /// Blocs are long lived and should not be rebuilt for subsequent calls. + /// Use [getBloc] which also handles caching. @protected T buildBloc(final Account account); + /// The [Provider] building the bloc [T] the currently active account. + /// + /// Blocs will not be disposed on disposal of the provider. You must handle + /// the [blocsCache] manually. Provider get blocProvider => Provider( create: (final context) { final accountsBloc = NeonProvider.of(context); @@ -62,10 +95,17 @@ abstract class AppImplementation }, ); + /// The count of unread notifications. + /// + /// If `null` no label will be displayed. BehaviorSubject? getUnreadCounter(final T bloc) => null; + /// The main page of this app. + /// + /// The framework will insert [blocProvider] into the widget tree before. Widget get page; + /// The drawer destination used in widgets like [NavigationDrawer]. NeonNavigationDestination destination(final BuildContext context) { final accountsBloc = NeonProvider.of(context); final account = accountsBloc.activeAccount.value!; @@ -89,7 +129,7 @@ abstract class AppImplementation /// Route for the app. /// - /// All pages of the app must be specified as subroutes. + /// All pages of the app must be specified as sub routes. /// If this is not [GoRoute] an initial route name must be specified by overriding [initialRouteName]. RouteBase get route; @@ -106,6 +146,10 @@ abstract class AppImplementation throw FlutterError('No name for the initial route provided.'); } + /// Builds the app icon. + /// + /// It is mandatory to provide a precompiled SVG under `assets/app.svg.vec`. + /// SVGs can be precompiled with `https://pub.dev/packages/vector_graphics_compiler` Widget buildIcon({ final double? size, final Color? color, @@ -145,7 +189,16 @@ abstract class AppImplementation int get hashCode => id.hashCode; } +/// Extension to find an app implementation by id in a Iterable. extension AppImplementationFind on Iterable { + /// Returns the first [AppImplementation] matching [appID] by [AppImplementation.id]. + /// + /// If no `AppImplementation` was found `null` is returned. AppImplementation? tryFind(final String? appID) => firstWhereOrNull((final app) => app.id == appID); + + /// Returns the first [AppImplementation] matching [appID] by [AppImplementation.id]. + /// + /// Throws a [StateError] if no `AppImplementation` was found. + /// Use [tryFind] to get a nullable result. AppImplementation find(final String appID) => firstWhere((final app) => app.id == appID); } diff --git a/packages/neon/neon/lib/src/models/disposable.dart b/packages/neon/neon/lib/src/models/disposable.dart index 9b9bc693..5e8e0228 100644 --- a/packages/neon/neon/lib/src/models/disposable.dart +++ b/packages/neon/neon/lib/src/models/disposable.dart @@ -11,6 +11,7 @@ abstract interface class Disposable { void dispose(); } +/// Extension on [Disposable] iterables. extension DisposableIterableBloc on Iterable { /// Calls [Disposable.dispose] on all entries. /// @@ -22,6 +23,7 @@ extension DisposableIterableBloc on Iterable { } } +/// Extension on [Disposable] maps. extension DisposableMapBloc on Map { /// Calls [Disposable.dispose] on all entries. /// diff --git a/packages/neon/neon/lib/src/models/label_builder.dart b/packages/neon/neon/lib/src/models/label_builder.dart index 82baa178..54fa4af7 100644 --- a/packages/neon/neon/lib/src/models/label_builder.dart +++ b/packages/neon/neon/lib/src/models/label_builder.dart @@ -1,3 +1,7 @@ import 'package:flutter/widgets.dart'; +/// The signature of a function generating a label. +/// +/// The `context` includes the [WidgetsApp]'s [Localizations] widget so that +/// this method can be used to produce a localized label. typedef LabelBuilder = String Function(BuildContext); diff --git a/packages/neon/neon/lib/src/models/notifications_interface.dart b/packages/neon/neon/lib/src/models/notifications_interface.dart index abcc1356..d56be887 100644 --- a/packages/neon/neon/lib/src/models/notifications_interface.dart +++ b/packages/neon/neon/lib/src/models/notifications_interface.dart @@ -3,8 +3,12 @@ import 'package:neon/src/bloc/bloc.dart'; import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/settings/models/options_collection.dart'; +/// The interface of the notifications client implementation. +/// +/// Use this to access the notifications client from other Neon clients. abstract interface class NotificationsAppInterface extends AppImplementation { + /// Creates a new notifications client. NotificationsAppInterface(); @override @@ -12,13 +16,20 @@ abstract interface class NotificationsAppInterface throw UnimplementedError(); } +/// The interface of the bloc used by the notifications client. abstract interface class NotificationsBlocInterface extends InteractiveBloc { + /// Creates a new notifications bloc. NotificationsBlocInterface(this.options); + /// The options for the notifications client. final NotificationsOptionsInterface options; + + /// Deletes the notification with the given [id]. void deleteNotification(final int id); } +/// The interface of the app options used by the notifications client. abstract interface class NotificationsOptionsInterface extends NextcloudAppOptions { + /// Creates the nextcloud app options for the notifications client. NotificationsOptionsInterface(super.storage); }