From 75254df069128dd331c327839608fd0a3982d4b8 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sat, 30 Sep 2023 08:17:11 +0200 Subject: [PATCH] docs(neon): document public facing api Signed-off-by: Nikolas Rimikis --- packages/neon/neon/lib/neon.dart | 3 + packages/neon/neon/lib/src/app.dart | 8 ++ packages/neon/neon/lib/src/bloc/bloc.dart | 25 ++++ .../neon/neon/lib/src/blocs/accounts.dart | 7 ++ packages/neon/neon/lib/src/blocs/apps.dart | 15 +++ .../neon/neon/lib/src/blocs/capabilities.dart | 1 + .../neon/neon/lib/src/models/account.dart | 21 ++++ .../neon/lib/src/models/account_cache.dart | 1 + .../lib/src/models/app_implementation.dart | 55 ++++++++- .../neon/neon/lib/src/models/disposable.dart | 2 + .../neon/lib/src/models/label_builder.dart | 4 + .../src/models/notifications_interface.dart | 11 ++ .../neon/lib/src/pages/account_settings.dart | 7 ++ packages/neon/neon/lib/src/pages/home.dart | 2 + .../neon/neon/lib/src/pages/settings.dart | 31 +++++ .../neon/neon/lib/src/platform/android.dart | 7 ++ .../neon/neon/lib/src/platform/linux.dart | 7 ++ .../neon/neon/lib/src/platform/platform.dart | 31 +++++ packages/neon/neon/lib/src/router.dart | 90 +++++++++++++- .../neon/lib/src/settings/models/option.dart | 3 + .../src/settings/models/options_category.dart | 4 + .../settings/models/options_collection.dart | 4 +- .../neon/lib/src/settings/models/storage.dart | 116 ++++++++++++++++++ .../utils/settings_export_helper.dart | 3 + .../widgets/account_settings_tile.dart | 6 + .../neon/neon/lib/src/theme/color_scheme.dart | 1 + packages/neon/neon/lib/src/theme/theme.dart | 18 +++ .../neon/lib/src/utils/account_options.dart | 7 ++ packages/neon/neon/lib/src/utils/global.dart | 1 + .../neon/lib/src/utils/global_options.dart | 91 ++++++++++++++ .../neon/lib/src/utils/global_popups.dart | 14 +++ .../neon/neon/lib/src/utils/hex_color.dart | 2 + .../neon/lib/src/utils/request_manager.dart | 1 + .../neon/neon/lib/src/utils/save_file.dart | 5 + .../neon/neon/lib/src/utils/validators.dart | 7 ++ .../neon/lib/src/widgets/account_tile.dart | 26 ++++ .../neon/neon/lib/src/widgets/app_bar.dart | 6 + .../src/widgets/app_implementation_icon.dart | 36 ++++++ .../neon/neon/lib/src/widgets/dialog.dart | 13 ++ .../neon/neon/lib/src/widgets/drawer.dart | 11 ++ .../widgets/linear_progress_indicator.dart | 11 ++ .../neon/lib/src/widgets/user_avatar.dart | 14 +++ .../neon/lib/src/widgets/validation_tile.dart | 18 +++ 43 files changed, 741 insertions(+), 5 deletions(-) diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index 6bf60d46..4b384c4b 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -20,6 +20,9 @@ import 'package:neon/src/utils/user_agent.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; +/// Runs Neon with the given [appImplementations]. +/// +/// Optionally provide a [theme] to set default style. Future runNeon({ required final Set appImplementations, required final NeonTheme theme, diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index 5513f751..e555434b 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -29,13 +29,21 @@ import 'package:quick_actions/quick_actions.dart'; import 'package:tray_manager/tray_manager.dart' as tray; import 'package:window_manager/window_manager.dart'; +/// Main Neon widget. +/// +/// Sets up all needed callbacks and creates a new [MaterialApp.router]. +/// This widget must be the first in the widget tree. @internal class NeonApp extends StatefulWidget { + /// Creates a new Neon app. const NeonApp({ required this.neonTheme, super.key, }); + /// The base Neon theme. + /// + /// This is used to seed the [AppTheme] used by [MaterialApp.theme]. final NeonTheme neonTheme; @override diff --git a/packages/neon/neon/lib/src/bloc/bloc.dart b/packages/neon/neon/lib/src/bloc/bloc.dart index d3b94912..676c0c2b 100644 --- a/packages/neon/neon/lib/src/bloc/bloc.dart +++ b/packages/neon/neon/lib/src/bloc/bloc.dart @@ -4,12 +4,23 @@ import 'package:flutter/foundation.dart'; import 'package:neon/src/models/disposable.dart'; import 'package:neon/src/utils/request_manager.dart'; +/// A Bloc for implementing the Business Logic Component pattern. +/// +/// This design pattern helps to separate presentation from business logic. +/// Following the BLoC pattern facilitates testability and reusability. +/// +/// If you are new to Flutter you might want to read: +/// https://www.didierboelens.com/blog/en/reactive-programming-streams-bloc abstract class Bloc implements Disposable { @override @mustCallSuper void dispose(); } +/// A bloc implementing basic data fetching. +/// +/// See: +/// * [Bloc]: for a generic bloc. abstract class InteractiveBloc extends Bloc { @override void dispose() { @@ -17,14 +28,28 @@ abstract class InteractiveBloc extends Bloc { } final _errorsStreamController = StreamController(); + + /// A stream of error events. late Stream errors = _errorsStreamController.stream.asBroadcastStream(); + /// Refreshes the state of the bloc. + /// + /// Commonly involves re fetching data from the server. FutureOr refresh(); + /// Adds an error to the [errors] state. + @protected void addError(final Object error) { _errorsStreamController.add(error); } + /// Wraps the action [call]. + /// + /// If [disableTimeout] is true [RequestManager] will apply the default + /// timeout. On success the state will be refreshed through the [refresh] + /// callback falling back to [this.refresh] if not supplied. Any errors will + /// be forwarded to [addError]. + @protected // ignore: avoid_void_async void wrapAction( final AsyncCallback call, { diff --git a/packages/neon/neon/lib/src/blocs/accounts.dart b/packages/neon/neon/lib/src/blocs/accounts.dart index 91e085e9..6c985611 100644 --- a/packages/neon/neon/lib/src/blocs/accounts.dart +++ b/packages/neon/neon/lib/src/blocs/accounts.dart @@ -21,6 +21,7 @@ import 'package:rxdart/rxdart.dart'; const _keyAccounts = 'accounts'; +/// Events for the [AccountsBloc]. @internal abstract interface class AccountsBlocEvents { /// Logs in the given [account]. @@ -45,6 +46,7 @@ abstract interface class AccountsBlocEvents { void setActiveAccount(final Account account); } +/// States for the [AccountsBloc]. @internal abstract interface class AccountsBlocStates { /// All registered accounts. @@ -59,7 +61,12 @@ abstract interface class AccountsBlocStates { BehaviorSubject get activeAccount; } +/// The Bloc responsible for managing the [Account]s class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocStates { + /// Creates a new account bloc. + /// + /// The last state will be loaded from storage and all necessary listeners + /// will be set up. AccountsBloc( this._globalOptions, this._allAppImplementations, diff --git a/packages/neon/neon/lib/src/blocs/apps.dart b/packages/neon/neon/lib/src/blocs/apps.dart index 411e1a77..e3651225 100644 --- a/packages/neon/neon/lib/src/blocs/apps.dart +++ b/packages/neon/neon/lib/src/blocs/apps.dart @@ -16,6 +16,7 @@ import 'package:nextcloud/nextcloud.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; +/// Events for the [AppsBloc]. @internal abstract interface class AppsBlocEvents { /// Sets the active app using the [appID]. @@ -25,21 +26,31 @@ abstract interface class AppsBlocEvents { void setActiveApp(final String appID, {final bool skipAlreadySet = false}); } +/// States for the [AppsBloc]. @internal abstract interface class AppsBlocStates { + /// A collection of clients used in the app drawer. + /// + /// It does not contain clients for that are specially handled like for the notifications. BehaviorSubject>> get appImplementations; + /// The interface of the notifications app. BehaviorSubject> get notificationsAppImplementation; + /// The currently active app. BehaviorSubject get activeApp; + /// A subject emitting an event when the notifications page should be opened. BehaviorSubject get openNotifications; + /// A collection of unsupported apps and their minimum required version. BehaviorSubject> get appVersions; } +/// The Bloc responsible for managing the [AppImplementation]s. @internal class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates { + /// Creates a new apps bloc. AppsBloc( this._capabilitiesBloc, this._accountsBloc, @@ -224,9 +235,13 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates } } + /// Returns the active [Bloc] for the given [appImplementation]. + /// + /// If no bloc exists yet a new one will be instantiated and cached in [AppImplementation.blocsCache]. T getAppBloc(final AppImplementation appImplementation) => appImplementation.getBloc(_account); + /// Returns the active [Bloc] for every registered [AppImplementation] wrapped in a Provider. List> get appBlocProviders => _allAppImplementations.map((final appImplementation) => appImplementation.blocProvider).toList(); } diff --git a/packages/neon/neon/lib/src/blocs/capabilities.dart b/packages/neon/neon/lib/src/blocs/capabilities.dart index 6b01bcd5..3aa4710f 100644 --- a/packages/neon/neon/lib/src/blocs/capabilities.dart +++ b/packages/neon/neon/lib/src/blocs/capabilities.dart @@ -18,6 +18,7 @@ abstract interface class CapabilitiesBlocStates { @internal class CapabilitiesBloc extends InteractiveBloc implements CapabilitiesBlocEvents, CapabilitiesBlocStates { + /// Creates a new capabilities bloc. CapabilitiesBloc( this._account, ) { diff --git a/packages/neon/neon/lib/src/models/account.dart b/packages/neon/neon/lib/src/models/account.dart index ddc11385..7765839d 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` can be identified `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` can be identified. + /// 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 bca76377..2e9ec98e 100644 --- a/packages/neon/neon/lib/src/models/app_implementation.dart +++ b/packages/neon/neon/lib/src/models/app_implementation.dart @@ -19,18 +19,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 final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, ); + /// 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` can be identified `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` can be identified. + /// 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..f83fb698 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); } diff --git a/packages/neon/neon/lib/src/pages/account_settings.dart b/packages/neon/neon/lib/src/pages/account_settings.dart index 4db34a89..1c619c5d 100644 --- a/packages/neon/neon/lib/src/pages/account_settings.dart +++ b/packages/neon/neon/lib/src/pages/account_settings.dart @@ -17,15 +17,22 @@ import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:nextcloud/provisioning_api.dart' as provisioning_api; +/// Account settings page. +/// +/// Displays Settings for an [Account]. Settings are specified as `Option`s. @internal class AccountSettingsPage extends StatelessWidget { + /// Creates a new account settings page for the given [account]. const AccountSettingsPage({ required this.bloc, required this.account, super.key, }); + /// The bloc managing the accounts and their settings. final AccountsBloc bloc; + + /// The account to display the settings for. final Account account; @override diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 7826c8f6..153707e1 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -19,8 +19,10 @@ import 'package:neon/src/widgets/unified_search_results.dart'; import 'package:nextcloud/core.dart' as core; import 'package:provider/provider.dart'; +/// The home page of Neon. @internal class HomePage extends StatefulWidget { + /// Creates a new home page. const HomePage({ super.key, }); diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index 7dc71852..370fb571 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -26,25 +26,56 @@ import 'package:neon/src/widgets/error.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; +/// Categories of the [SettingsPage]. +/// +/// Used with [SettingsPage.initialCategory] to scroll to a specific section. +/// Values are in order of appearance but are not guaranteed to be included on +/// the settings page. @internal enum SettingsCategories { + /// `NextcloudAppOptions` category. + /// + /// Each activated `AppImplementation` has an entry. apps, + + /// Theming category. theme, + + /// Device navigation category. navigation, + + /// Push notifications category. pushNotifications, + + /// Startup category. startup, + + /// SystemTray category. systemTray, + + /// Account management category. + /// + /// Also includes the `AccountSpecificOptions`. accounts, + + /// Other category. + /// + /// Contains legal information and various links. other, } +/// Settings page. +/// +/// Settings are specified as `Option`s. @internal class SettingsPage extends StatefulWidget { + /// Creates a new settings page. const SettingsPage({ this.initialCategory, super.key, }); + /// The category to show after creation. final SettingsCategories? initialCategory; @override diff --git a/packages/neon/neon/lib/src/platform/android.dart b/packages/neon/neon/lib/src/platform/android.dart index 9dceed83..bf93e464 100644 --- a/packages/neon/neon/lib/src/platform/android.dart +++ b/packages/neon/neon/lib/src/platform/android.dart @@ -1,13 +1,20 @@ import 'package:meta/meta.dart'; +import 'package:neon/src/platform/linux.dart'; import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/utils/exceptions.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +/// Android specific platform information. +/// +/// See: +/// * [NeonPlatform] to initialize and acquire an instance +/// * [LinuxNeonPlatform] for the linux implementation @immutable @internal class AndroidNeonPlatform implements NeonPlatform { + /// Creates a new android Neon platform. const AndroidNeonPlatform(); @override diff --git a/packages/neon/neon/lib/src/platform/linux.dart b/packages/neon/neon/lib/src/platform/linux.dart index 4c3f5b2c..b1191f04 100644 --- a/packages/neon/neon/lib/src/platform/linux.dart +++ b/packages/neon/neon/lib/src/platform/linux.dart @@ -1,13 +1,20 @@ import 'dart:io'; import 'package:meta/meta.dart'; +import 'package:neon/src/platform/android.dart'; import 'package:neon/src/platform/platform.dart'; import 'package:path/path.dart' as p; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +/// Android specific platform information. +/// +/// See: +/// * [NeonPlatform] to initialize and acquire an instance +/// * [AndroidNeonPlatform] for the android implementation @immutable @internal class LinuxNeonPlatform implements NeonPlatform { + /// Creates a new linux Neon platform. const LinuxNeonPlatform(); @override diff --git a/packages/neon/neon/lib/src/platform/platform.dart b/packages/neon/neon/lib/src/platform/platform.dart index 6393ea2a..924c8666 100644 --- a/packages/neon/neon/lib/src/platform/platform.dart +++ b/packages/neon/neon/lib/src/platform/platform.dart @@ -6,8 +6,15 @@ import 'package:neon/src/platform/android.dart'; import 'package:neon/src/platform/linux.dart'; /// Implements platform specific functionality and exposes the availability of certain features. +/// +/// [NeonPlatform.setup] mus be called and completed before acquiring the [instance]. +/// +/// See: +/// * [AndroidNeonPlatform] for the android implementation +/// * [LinuxNeonPlatform] for the linux implementation @immutable abstract interface class NeonPlatform { + /// Initializes the platform with the given mocked [platform]. @visibleForTesting factory NeonPlatform.mocked(final NeonPlatform platform) => _platform = platform; @@ -45,18 +52,39 @@ abstract interface class NeonPlatform { return _platform!; } + /// Whether this platform supports web views. + /// + /// The support depends on `https://pub.dev/packages/webview_flutter`. abstract final bool canUseWebView; + /// Whether this platform can use quick actions. + /// + /// The support depends on `https://pub.dev/packages/quick_actions`. abstract final bool canUseQuickActions; + /// Whether this platform support system trays. + /// + /// The support depends on `https://pub.dev/packages/tray_manager`. abstract final bool canUseSystemTray; + /// Whether this platform supports window managers. + /// + /// The support depends on `https://pub.dev/packages/window_manager`. abstract final bool canUseWindowManager; + /// Whether this platform can use the camera. + /// + /// The support depends on `https://pub.dev/packages/camera`. abstract final bool canUseCamera; + /// Whether this platform can use push notifications. + /// + /// The support depends on `https://pub.dev/packages/unifiedpush`. abstract final bool canUsePushNotifications; + /// Wether this platform supports a native sharing option. + /// + /// The support depends on `https://pub.dev/packages/share_plus`. abstract final bool canUseSharing; /// Whether this platform should use file dialog. @@ -64,7 +92,10 @@ abstract interface class NeonPlatform { /// This is needed to compensate lacking support of `https://pub.dev/packages/file_picker`. abstract final bool shouldUseFileDialog; + /// Returns the path to a directory where the application may access top + /// level storage. FutureOr get userAccessibleAppDataPath; + /// Initializes this platform. FutureOr init(); } diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart index 83c20b18..1860b21f 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -24,6 +24,7 @@ import 'package:neon/src/utils/stream_listenable.dart'; part 'router.g.dart'; +/// Internal router for the Neon framework. @internal GoRouter buildAppRouter({ required final GlobalKey navigatorKey, @@ -68,12 +69,19 @@ Page _buildErrorPage(final BuildContext context, final GoRouterState state ), ); +/// {@template AppRoutes.AccountSettingsRoute} +/// Route for the the [AccountSettingsPage]. +/// {@endtemplate} @immutable class AccountSettingsRoute extends GoRouteData { + /// {@macro AppRoutes.AccountSettingsRoute} const AccountSettingsRoute({ required this.accountID, }); + /// The id of the account to show the settings for. + /// + /// Passed to [AccountSettingsPage.account]. final String accountID; @override @@ -88,6 +96,9 @@ class AccountSettingsRoute extends GoRouteData { } } +/// {@template AppRoutes.HomeRoute} +/// Route for the the [HomePage]. +/// {@endtemplate} @TypedGoRoute( path: '/', name: 'home', @@ -128,6 +139,7 @@ class AccountSettingsRoute extends GoRouteData { ) @immutable class HomeRoute extends GoRouteData { + /// {@macro AppRoutes.HomeRoute} const HomeRoute(); @override @@ -139,6 +151,13 @@ class HomeRoute extends GoRouteData { } } +/// {@template AppRoutes.LoginRoute} +/// Route for the the initial [LoginPage]. +/// +/// All routes related to the login flow are subroutes of this. +/// All subroutes redirect to subroutes of [HomeRoute] if a at least one +/// account is already logged in and further accounts should be added. +/// {@endtemplate} @TypedGoRoute( path: '/login', name: 'login', @@ -159,6 +178,7 @@ class HomeRoute extends GoRouteData { ) @immutable class LoginRoute extends GoRouteData { + /// {@macro AppRoutes.LoginRoute} const LoginRoute(); @override @@ -176,12 +196,22 @@ class LoginRoute extends GoRouteData { } } +/// {@template AppRoutes.LoginFlowRoute} +/// Route for the the [LoginFlowPage]. +/// +/// Redirects to [_AddAccountFlowRoute] when at least one account is already +/// logged in. +/// {@endtemplate} @immutable class LoginFlowRoute extends GoRouteData { + /// {@macro AppRoutes.LoginFlowRoute} const LoginFlowRoute({ required this.serverUrl, }); + /// {@template AppRoutes.LoginFlow.serverUrl} + /// The url of the server to initiate the login flow for. + /// {@endtemplate} final Uri serverUrl; @override @@ -199,8 +229,15 @@ class LoginFlowRoute extends GoRouteData { } } +/// {@template AppRoutes.LoginQRcodeRoute} +/// Route for the the [LoginQRcodePage]. +/// +/// Redirects to [_AddAccountQRcodeRoute] when at least one account is already +/// logged in. +/// {@endtemplate} @immutable class LoginQRcodeRoute extends GoRouteData { + /// {@macro AppRoutes.LoginQRcodeRoute} const LoginQRcodeRoute(); @override @@ -218,22 +255,47 @@ class LoginQRcodeRoute extends GoRouteData { } } +/// {@template AppRoutes.LoginCheckServerStatusRoute} +/// Route for the the [LoginCheckServerStatusPage]. +/// +/// Redirects to [_AddAccountCheckServerStatusRoute] when at least one account +/// is already logged in. +/// {@endtemplate} @immutable class LoginCheckServerStatusRoute extends GoRouteData { + /// {@macro AppRoutes.LoginCheckServerStatusRoute} + /// + /// [loginName] and [password] must both be null. + /// Use [LoginCheckServerStatusRoute.withCredentials] to specify credentials. const LoginCheckServerStatusRoute({ required this.serverUrl, this.loginName, this.password, - }); - + }) : assert( + loginName == null && password == null, + 'loginName and password must be null. Use LoginCheckServerStatusRoute.withCredentials instead.', + ); + + /// {@macro AppRoutes.LoginCheckServerStatusRoute} + /// + /// See [LoginCheckServerStatusRoute] for a route without initial credentials. const LoginCheckServerStatusRoute.withCredentials({ required this.serverUrl, required String this.loginName, required String this.password, }) : assert(!kIsWeb, 'Might leak the password to the browser history'); + /// {@macro AppRoutes.LoginFlow.serverUrl} final Uri serverUrl; + + /// {@template AppRoutes.LoginFlow.loginName} + /// The login name of the credentials. + /// {@endtemplate} final String? loginName; + + /// {@template AppRoutes.LoginFlow.password} + /// The password of the credentials. + /// {@endtemplate} final String? password; @override @@ -273,16 +335,27 @@ class LoginCheckServerStatusRoute extends GoRouteData { } } +/// {@template AppRoutes.LoginCheckAccountRoute} +/// Route for the the [LoginCheckAccountPage]. +/// +/// Redirects to the +/// {@endtemplate} @immutable class LoginCheckAccountRoute extends GoRouteData { + /// {@macro AppRoutes.LoginCheckAccountRoute} const LoginCheckAccountRoute({ required this.serverUrl, required this.loginName, required this.password, }) : assert(!kIsWeb, 'Might leak the password to the browser history'); + /// {@macro AppRoutes.LoginFlow.serverUrl} final Uri serverUrl; + + /// {@macro AppRoutes.LoginFlow.loginName} final String loginName; + + /// {@macro AppRoutes.LoginFlow.password} final String password; @override @@ -364,12 +437,17 @@ class _AddAccountCheckAccountRoute extends LoginCheckAccountRoute { String get password => super.password; } +/// {@template AppRoutes.NextcloudAppSettingsRoute} +/// Route for the the [NextcloudAppSettingsPage]. +/// {@endtemplate} @immutable class NextcloudAppSettingsRoute extends GoRouteData { + /// {@macro AppRoutes.NextcloudAppSettingsRoute} const NextcloudAppSettingsRoute({ required this.appid, }); + /// The id of the app to display the settings for. final String appid; @override @@ -381,9 +459,15 @@ class NextcloudAppSettingsRoute extends GoRouteData { } } +/// {@template AppRoutes.SettingsRoute} +/// Route for the the [SettingsPage]. +/// {@endtemplate} @immutable class SettingsRoute extends GoRouteData { - const SettingsRoute({this.initialCategory}); + /// {@macro AppRoutes.SettingsRoute} + const SettingsRoute({ + this.initialCategory, + }); /// The initial category to show. final SettingsCategories? initialCategory; diff --git a/packages/neon/neon/lib/src/settings/models/option.dart b/packages/neon/neon/lib/src/settings/models/option.dart index af78a9d0..16557561 100644 --- a/packages/neon/neon/lib/src/settings/models/option.dart +++ b/packages/neon/neon/lib/src/settings/models/option.dart @@ -216,6 +216,9 @@ class SelectOption extends Option { /// * [value] for the currently selected one Map get values => _values; + /// Updates the collection of possible values. + /// + /// It is up to the caller to also change the [value] if it is no longer supported. set values(final Map newValues) { if (_values == newValues) { return; diff --git a/packages/neon/neon/lib/src/settings/models/options_category.dart b/packages/neon/neon/lib/src/settings/models/options_category.dart index be93457d..bbc84518 100644 --- a/packages/neon/neon/lib/src/settings/models/options_category.dart +++ b/packages/neon/neon/lib/src/settings/models/options_category.dart @@ -1,11 +1,15 @@ import 'package:meta/meta.dart'; import 'package:neon/src/models/label_builder.dart'; +import 'package:neon/src/settings/models/option.dart'; +/// Category of an [Option]. @immutable class OptionsCategory { + /// Creates a new Category. const OptionsCategory({ required this.name, }); + /// Builder function for the category name. final LabelBuilder name; } diff --git a/packages/neon/neon/lib/src/settings/models/options_collection.dart b/packages/neon/neon/lib/src/settings/models/options_collection.dart index 37b99377..247c1171 100644 --- a/packages/neon/neon/lib/src/settings/models/options_collection.dart +++ b/packages/neon/neon/lib/src/settings/models/options_collection.dart @@ -7,6 +7,7 @@ import 'package:neon/src/settings/models/storage.dart'; /// Collection of [Option]s. abstract class OptionsCollection implements Exportable, Disposable { + /// Creates a new collection of options. OptionsCollection(this.storage); /// Storage backend to use. @@ -56,8 +57,9 @@ abstract class OptionsCollection implements Exportable, Disposable { } } -/// OptionsCollection for a neon app. +/// OptionsCollection primarily used by `AppImplementation`s. abstract class NextcloudAppOptions extends OptionsCollection { + /// Creates a new Nextcloud options collection. NextcloudAppOptions(super.storage); /// Collection of categories to display the options in the settings. diff --git a/packages/neon/neon/lib/src/settings/models/storage.dart b/packages/neon/neon/lib/src/settings/models/storage.dart index 9dcd4af1..e00730f2 100644 --- a/packages/neon/neon/lib/src/settings/models/storage.dart +++ b/packages/neon/neon/lib/src/settings/models/storage.dart @@ -2,32 +2,87 @@ import 'package:meta/meta.dart'; import 'package:nextcloud/ids.dart'; import 'package:shared_preferences/shared_preferences.dart'; +/// Storage interface used by `Option`s. +/// +/// Mimics the interface of [SharedPreferences]. +/// +/// See: +/// * [SingleValueStorage] for a storage that saves a single value. +/// * [AppStorage] for a storage that fully implements the [SharedPreferences] interface. +/// * [NeonStorage] that manages the storage backend. @internal abstract interface class SettingsStorage { + /// {@template NeonStorage.getString} + /// Reads a value from persistent storage, throwing an exception if it's not a String. + /// {@endtemplate} String? getString(final String key); + /// {@template NeonStorage.setString} + /// Saves a string [value] to persistent storage in the background. + /// + /// Note: Due to limitations in Android's SharedPreferences, + /// values cannot start with any one of the following: + /// + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + /// {@endtemplate} Future setString(final String key, final String value); + /// {@template NeonStorage.getBool} + /// Reads a value from persistent storage, throwing an exception if it's not a bool. + /// {@endtemplate} bool? getBool(final String key); + /// {@template NeonStorage.setBool} + /// Saves a boolean [value] to persistent storage in the background. + /// {@endtemplate} + // ignore: avoid_positional_boolean_parameters // ignore: avoid_positional_boolean_parameters Future setBool(final String key, final bool value); + /// {@template NeonStorage.remove} + /// Removes an entry from persistent storage. + /// {@endtemplate} Future remove(final String key); } +/// Interface of a storable element. +/// +/// Usually used in enhanced enums to ensure uniqueness of the storage keys. abstract interface class Storable { + /// The key of this storage element. String get value; } +/// Unique storage keys. +/// +/// Required by the users of the [NeonStorage] storage backend. +/// +/// See: +/// * [AppStorage] for a storage that fully implements the [SharedPreferences] interface. +/// * [SettingsStorage] for the public interface used in `Option`s. @internal enum StorageKeys implements Storable { + /// The key for the `AppImplementation`s. apps._('app'), + + /// The key for the `Account`s and their `AccountSpecificOptions`. accounts._('accounts'), + + /// The key for the `GlobalOptions`. global._('global'), + + /// The key for the `AccountsBloc` last used account. lastUsedAccount._('last-used-account'), + + /// The key used by the `PushNotificationsBloc` to persist the last used endpoint. lastEndpoint._('last-endpoint'), + + /// The key for the `FirstLaunchBloc`. firstLaunch._('first-launch'), + + /// The key for the `PushUtils`. notifications._(AppIDs.notifications); const StorageKeys._(this.value); @@ -36,6 +91,14 @@ enum StorageKeys implements Storable { final String value; } +/// Neon storage that manages the storage backend. +/// +/// [init] must be called and completed before accessing individual storages. +/// +/// See: +/// * [SingleValueStorage] for a storage that saves a single value. +/// * [AppStorage] for a storage that fully implements the [SharedPreferences] interface. +/// * [SettingsStorage] for the public interface used in `Option`s. @internal final class NeonStorage { const NeonStorage._(); @@ -46,6 +109,7 @@ final class NeonStorage { /// Make sure it has been initialized with [init] before. static SharedPreferences? _sharedPreferences; + /// Initializes the database instance with a mocked value. @visibleForTesting // ignore: use_setters_to_change_properties static void mock(final SharedPreferences mock) => _sharedPreferences = mock; @@ -61,6 +125,9 @@ final class NeonStorage { _sharedPreferences = await SharedPreferences.getInstance(); } + /// Returns the database instance. + /// + /// Throws a `StateError` if [init] has not completed. @visibleForTesting static SharedPreferences get database { if (_sharedPreferences == null) { @@ -73,45 +140,85 @@ final class NeonStorage { } } +/// A storage that saves a single value. +/// +/// [NeonStorage.init] must be called and completed before accessing individual values. +/// +/// See: +/// * [NeonStorage] to initialize the storage backend. +/// * [AppStorage] for a storage that fully implements the [SharedPreferences] interface. +/// * [SettingsStorage] for the public interface used in `Option`s. @immutable @internal final class SingleValueStorage { + /// Creates a new storage for a single value. const SingleValueStorage(this.key); + /// The key used by the storage backend. final StorageKeys key; + /// {@macro NeonStorage.containsKey} bool hasValue() => NeonStorage.database.containsKey(key.value); + /// {@macro NeonStorage.remove} Future remove() => NeonStorage.database.remove(key.value); + /// {@macro NeonStorage.getString} String? getString() => NeonStorage.database.getString(key.value); + /// {@macro NeonStorage.setString} Future setString(final String value) => NeonStorage.database.setString(key.value, value); + /// {@macro NeonStorage.getBool} bool? getBool() => NeonStorage.database.getBool(key.value); + /// {@macro NeonStorage.setBool} // ignore: avoid_positional_boolean_parameters Future setBool(final bool value) => NeonStorage.database.setBool(key.value, value); + /// {@macro NeonStorage.getStringList} List? getStringList() => NeonStorage.database.getStringList(key.value); + /// {@macro NeonStorage.setStringList} Future setStringList(final List value) => NeonStorage.database.setStringList(key.value, value); } +/// A storage that can save a group of values. +/// +/// Implements the interface of [SharedPreferences]. +/// [NeonStorage.init] must be called and completed before accessing individual values. +/// +/// See: +/// * [NeonStorage] to initialize the storage backend. +/// * [SingleValueStorage] for a storage that saves a single value. +/// * [SettingsStorage] for the public interface used in `Option`s. @immutable @internal final class AppStorage implements SettingsStorage { + /// Creates a new app storage. const AppStorage( this.groupKey, [ this.suffix, ]); + /// The group key for this app storage. + /// + /// Keys are formatted with [formatKey] final StorageKeys groupKey; + /// The optional suffix of the storage key. + /// + /// Used to differentiate between multiple AppStorages with the same [groupKey]. final String? suffix; + /// Returns the id for this app storage. + /// + /// Uses the [suffix] and falling back to the [groupKey] if not present. + /// This uniquely identifies the storage and is used in `Exportable` classes. String get id => suffix ?? groupKey.value; + /// Concatenates the [groupKey], [suffix] and [key] to build a unique key + /// used in the storage backend. @visibleForTesting String formatKey(final String key) { if (suffix != null) { @@ -121,6 +228,9 @@ final class AppStorage implements SettingsStorage { return '${groupKey.value}-$key'; } + /// {@template NeonStorage.containsKey} + /// Returns true if the persistent storage contains the given [key]. + /// {@endtemplate} bool containsKey(final String key) => NeonStorage.database.containsKey(formatKey(key)); @override @@ -138,8 +248,14 @@ final class AppStorage implements SettingsStorage { @override Future setBool(final String key, final bool value) => NeonStorage.database.setBool(formatKey(key), value); + /// {@template NeonStorage.getStringList} + /// Reads a set of string values from persistent storage, throwing an exception if it's not a string set. + /// {@endtemplate} List? getStringList(final String key) => NeonStorage.database.getStringList(formatKey(key)); + /// {@template NeonStorage.setStringList} + /// Saves a list of strings [value] to persistent storage in the background. + /// {@endtemplate} Future setStringList(final String key, final List value) => NeonStorage.database.setStringList(formatKey(key), value); } diff --git a/packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart b/packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart index acda7208..528c0ea1 100644 --- a/packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart +++ b/packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart @@ -22,6 +22,7 @@ import 'package:neon/src/settings/models/storage.dart'; @internal @immutable class SettingsExportHelper { + /// Creates a new settings exporter for the given [exportables]. const SettingsExportHelper({ required this.exportables, }); @@ -77,6 +78,7 @@ class SettingsExportHelper { @internal @immutable class AppImplementationsExporter implements Exportable { + /// Creates a new [AppImplementation] exporter. const AppImplementationsExporter(this.appImplementations); /// List of apps to export. @@ -113,6 +115,7 @@ class AppImplementationsExporter implements Exportable { @internal @immutable class AccountsBlocExporter implements Exportable { + /// Creates a new [Account] exporter. const AccountsBlocExporter(this.accountsBloc); /// AccountsBloc containing the accounts to export. diff --git a/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart b/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart index 8ec81116..b444ab16 100644 --- a/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart +++ b/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart @@ -4,8 +4,10 @@ import 'package:neon/src/models/account.dart'; import 'package:neon/src/settings/widgets/settings_tile.dart'; import 'package:neon/src/widgets/account_tile.dart'; +/// An [NeonAccountTile] used inside a settings list. @internal class AccountSettingsTile extends SettingsTile { + /// Creates a new account settings tile. const AccountSettingsTile({ required this.account, this.trailing, @@ -13,9 +15,13 @@ class AccountSettingsTile extends SettingsTile { super.key, }); + /// {@macro neon.AccountTile.account} final Account account; + /// {@macro neon.AccountTile.trailing} final Widget? trailing; + + /// {@macro neon.AccountTile.onTap} final GestureTapCallback? onTap; @override diff --git a/packages/neon/neon/lib/src/theme/color_scheme.dart b/packages/neon/neon/lib/src/theme/color_scheme.dart index e670dd52..c1100a4a 100644 --- a/packages/neon/neon/lib/src/theme/color_scheme.dart +++ b/packages/neon/neon/lib/src/theme/color_scheme.dart @@ -5,6 +5,7 @@ import 'package:neon/src/theme/neon.dart'; /// A ColorScheme used in the [NeonTheme]. @immutable class NeonColorScheme { + /// Creates a new neon color scheme. const NeonColorScheme({ this.primary = NcColors.primary, }); diff --git a/packages/neon/neon/lib/src/theme/theme.dart b/packages/neon/neon/lib/src/theme/theme.dart index a2692d61..54175c78 100644 --- a/packages/neon/neon/lib/src/theme/theme.dart +++ b/packages/neon/neon/lib/src/theme/theme.dart @@ -5,9 +5,11 @@ import 'package:neon/src/theme/neon.dart'; import 'package:neon/src/utils/hex_color.dart'; import 'package:nextcloud/core.dart' as core; +/// Custom theme used for the Neon app. @internal @immutable class AppTheme { + /// Creates a new Neon app theme. const AppTheme( this.nextcloudTheme, { required this.neonTheme, @@ -16,10 +18,19 @@ class AppTheme { this.appThemes, }) : keepOriginalAccentColor = nextcloudTheme == null || keepOriginalAccentColor; + /// The theme provided by Nextcloud. final core.ThemingPublicCapabilities_Theming? nextcloudTheme; + + /// Whether to force the use of the Nextcloud accent color. final bool keepOriginalAccentColor; + + /// Whether to use [Colors.black] in the dark theme. final bool oledAsDark; + + /// The theme extensions provided by the `AppImplementation`s. final Iterable? appThemes; + + /// The base theme for the Neon app. final NeonTheme neonTheme; ColorScheme _buildColorScheme(final Brightness brightness) { @@ -56,7 +67,14 @@ class AppTheme { ); } + /// Returns a new theme for [Brightness.light]. + /// + /// Used in [MaterialApp.theme]. ThemeData get lightTheme => _getTheme(Brightness.light); + + /// Returns a new theme for [Brightness.dark]. + /// + /// Used in [MaterialApp.darkTheme]. ThemeData get darkTheme => _getTheme(Brightness.dark); static const _snackBarTheme = SnackBarThemeData( diff --git a/packages/neon/neon/lib/src/utils/account_options.dart b/packages/neon/neon/lib/src/utils/account_options.dart index 225c2245..4af2fb4f 100644 --- a/packages/neon/neon/lib/src/utils/account_options.dart +++ b/packages/neon/neon/lib/src/utils/account_options.dart @@ -5,9 +5,11 @@ import 'package:neon/src/settings/models/option.dart'; import 'package:neon/src/settings/models/options_collection.dart'; import 'package:neon/src/settings/models/storage.dart'; +/// Account related options. @internal @immutable class AccountSpecificOptions extends OptionsCollection { + /// Creates a new account options collection. AccountSpecificOptions( super.storage, this._appsBloc, @@ -30,6 +32,9 @@ class AccountSpecificOptions extends OptionsCollection { initialApp, ]; + /// The initial app to show on app start. + /// + /// Defaults to `null` letting the framework choose one. late final initialApp = SelectOption( storage: storage, key: AccountOptionKeys.initialApp, @@ -39,8 +44,10 @@ class AccountSpecificOptions extends OptionsCollection { ); } +/// Storage keys for the [AccountSpecificOptions]. @internal enum AccountOptionKeys implements Storable { + /// The storage key for [AccountSpecificOptions.initialApp] initialApp._('initial-app'); const AccountOptionKeys._(this.value); diff --git a/packages/neon/neon/lib/src/utils/global.dart b/packages/neon/neon/lib/src/utils/global.dart index 4305e33f..553c1fb8 100644 --- a/packages/neon/neon/lib/src/utils/global.dart +++ b/packages/neon/neon/lib/src/utils/global.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import 'package:neon/src/models/push_notification.dart'; +/// Global states handling notification callbacks. @internal class Global { const Global._(); diff --git a/packages/neon/neon/lib/src/utils/global_options.dart b/packages/neon/neon/lib/src/utils/global_options.dart index c2d93699..deb00227 100644 --- a/packages/neon/neon/lib/src/utils/global_options.dart +++ b/packages/neon/neon/lib/src/utils/global_options.dart @@ -11,11 +11,14 @@ import 'package:neon/src/settings/models/storage.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; +/// The id of the NextPush unified push distributor. const unifiedPushNextPushID = 'org.unifiedpush.distributor.nextpush'; +/// Global options for the Neon framework. @internal @immutable class GlobalOptions extends OptionsCollection { + /// Creates a new global options collection. GlobalOptions( this._packageInfo, ) : super(const AppStorage(StorageKeys.global)) { @@ -90,6 +93,9 @@ class GlobalOptions extends OptionsCollection { rememberLastUsedAccount.removeListener(_rememberLastUsedAccountListener); } + /// Updates the available values of [initialAccount]. + /// + /// If the current `initialAccount` is not supported anymore the option will be reset. void updateAccounts(final List accounts) { initialAccount.values = Map.fromEntries( accounts.map( @@ -102,6 +108,10 @@ class GlobalOptions extends OptionsCollection { } } + /// Updates the values of [pushNotificationsDistributor]. + /// + /// If the new `distributors` does not contain the currently active one + /// both [pushNotificationsDistributor] and [pushNotificationsEnabled] will be reset. void updateDistributors(final List distributors) { pushNotificationsDistributor.values = Map.fromEntries( distributors.map( @@ -117,6 +127,7 @@ class GlobalOptions extends OptionsCollection { } } + /// The theme mode of the app implementing the Neon framework. late final themeMode = SelectOption( storage: storage, key: GlobalOptionKeys.themeMode, @@ -129,6 +140,10 @@ class GlobalOptions extends OptionsCollection { }, ); + /// Whether the [ThemeMode.dark] should use a plain black background. + /// + /// This is commonly used on oled devices. + /// Defaults to `false`. late final themeOLEDAsDark = ToggleOption( storage: storage, key: GlobalOptionKeys.themeOLEDAsDark, @@ -136,6 +151,9 @@ class GlobalOptions extends OptionsCollection { defaultValue: false, ); + /// Whether the `ColorScheme` should keep the Nextcloud you provided color. + /// + /// Defaults to `false` generating a material3 style color. late final themeKeepOriginalAccentColor = ToggleOption( storage: storage, key: GlobalOptionKeys.themeKeepOriginalAccentColor, @@ -143,6 +161,12 @@ class GlobalOptions extends OptionsCollection { defaultValue: false, ); + /// Whether to enable the push notifications plugin. + /// + /// Setting this option to true will request notification access. + /// Disabling this option will reset [pushNotificationsDistributor]. + /// + /// Defaults to `false`. late final pushNotificationsEnabled = ToggleOption( storage: storage, key: GlobalOptionKeys.pushNotificationsEnabled, @@ -150,6 +174,7 @@ class GlobalOptions extends OptionsCollection { defaultValue: false, ); + /// The registered distributor for push notifications. late final pushNotificationsDistributor = SelectOption.depend( storage: storage, key: GlobalOptionKeys.pushNotificationsDistributor, @@ -159,6 +184,13 @@ class GlobalOptions extends OptionsCollection { enabled: pushNotificationsEnabled, ); + /// Whether to start the app implementing Neon minimized. + /// + /// Defaults to `false`. + /// + /// See: + /// * [minimizeInsteadOfExit]: for an option to minimize instead of closing the app. + /// *[systemTrayHideToTrayWhenMinimized]: to minimize the app to system tray. late final startupMinimized = ToggleOption( storage: storage, key: GlobalOptionKeys.startupMinimized, @@ -166,6 +198,13 @@ class GlobalOptions extends OptionsCollection { defaultValue: false, ); + /// Whether to minimize app implementing Neon instead of closing. + /// + /// Defaults to `false`. + /// + /// See: + /// * [startupMinimized]: for an option to startup in the minimized state. + /// *[systemTrayHideToTrayWhenMinimized]: to minimize the app to system tray. late final startupMinimizeInsteadOfExit = ToggleOption( storage: storage, key: GlobalOptionKeys.startupMinimizeInsteadOfExit, @@ -175,6 +214,12 @@ class GlobalOptions extends OptionsCollection { // TODO: Autostart option + /// Whether to enable the system tray. + /// + /// Defaults to `false`. + /// + /// See: + /// *[systemTrayHideToTrayWhenMinimized]: to minimize the app to system tray. late final systemTrayEnabled = ToggleOption( storage: storage, key: GlobalOptionKeys.systemTrayEnabled, @@ -182,6 +227,15 @@ class GlobalOptions extends OptionsCollection { defaultValue: false, ); + /// Whether to minimize to the system tray or not. + /// + /// Requires [systemTrayEnabled] to be true. + /// Defaults to `true`. + /// + /// See: + /// * [systemTrayEnabled]: to enable the system tray. + /// * [startupMinimized]: for an option to startup in the minimized state. + /// * [minimizeInsteadOfExit]: for an option to minimize instead of closing the app. late final systemTrayHideToTrayWhenMinimized = ToggleOption.depend( storage: storage, key: GlobalOptionKeys.systemTrayHideToTrayWhenMinimized, @@ -190,6 +244,10 @@ class GlobalOptions extends OptionsCollection { enabled: systemTrayEnabled, ); + /// Whether to remember the last active account. + /// + /// Enabling this option will reset the [initialAccount]. + /// Defaults to `true`. late final rememberLastUsedAccount = ToggleOption( storage: storage, key: GlobalOptionKeys.rememberLastUsedAccount, @@ -197,6 +255,7 @@ class GlobalOptions extends OptionsCollection { defaultValue: true, ); + /// The initial account to use when opening the app. late final initialAccount = SelectOption( storage: storage, key: GlobalOptionKeys.initialAccount, @@ -219,19 +278,43 @@ class GlobalOptions extends OptionsCollection { ); } +/// The storage keys for the [GlobalOptions]. @internal enum GlobalOptionKeys implements Storable { + /// The storage key for [GlobalOptions.themeMode] themeMode._('theme-mode'), + + /// The storage key for [GlobalOptions.themeOLEDAsDark] themeOLEDAsDark._('theme-oled-as-dark'), + + /// The storage key for [GlobalOptions.themeKeepOriginalAccentColor] themeKeepOriginalAccentColor._('theme-keep-original-accent-color'), + + /// The storage key for [GlobalOptions.pushNotificationsEnabled] pushNotificationsEnabled._('push-notifications-enabled'), + + /// The storage key for [GlobalOptions.pushNotificationsDistributor] pushNotificationsDistributor._('push-notifications-distributor'), + + /// The storage key for [GlobalOptions.startupMinimized] startupMinimized._('startup-minimized'), + + /// The storage key for [GlobalOptions.startupMinimizeInsteadOfExit] startupMinimizeInsteadOfExit._('startup-minimize-instead-of-exit'), + + /// The storage key for [GlobalOptions.systemTrayEnabled] systemTrayEnabled._('system-tray-enabled'), + + /// The storage key for [GlobalOptions.systemTrayHideToTrayWhenMinimized] systemTrayHideToTrayWhenMinimized._('system-tray-hide-to-tray-when-minimized'), + + /// The storage key for [GlobalOptions.rememberLastUsedAccount] rememberLastUsedAccount._('remember-last-used-account'), + + /// The storage key for [GlobalOptions.initialAccount] initialAccount._('initial-account'), + + /// The storage key for [GlobalOptions.navigationMode] navigationMode._('navigation-mode'); const GlobalOptionKeys._(this.value); @@ -240,8 +323,16 @@ enum GlobalOptionKeys implements Storable { final String value; } +/// App navigation modes. @internal enum NavigationMode { + /// Drawer behind a hamburger menu. + /// + /// The default for small screen sizes. drawer, + + /// Persistent drawer on the leading edge. + /// + /// The default on large screen sizes. drawerAlwaysVisible, } diff --git a/packages/neon/neon/lib/src/utils/global_popups.dart b/packages/neon/neon/lib/src/utils/global_popups.dart index 54ab7e7c..897e28f5 100644 --- a/packages/neon/neon/lib/src/utils/global_popups.dart +++ b/packages/neon/neon/lib/src/utils/global_popups.dart @@ -12,15 +12,23 @@ import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +/// Singleton class managing global popups. @internal class GlobalPopups { + /// Returns the current instance. + /// + /// Instantiates a new one if not yet present. factory GlobalPopups() => instance ??= GlobalPopups._(); + /// Returns the current instance. + /// + /// Instantiates a new one with a [mock]ed value if not yet present. @visibleForTesting factory GlobalPopups.mocked(final GlobalPopups mock) => instance ??= mock; GlobalPopups._(); + /// The instance of this singleton. @visibleForTesting static GlobalPopups? instance; @@ -28,6 +36,9 @@ class GlobalPopups { late BuildContext _context; final _subscriptions = >[]; + /// Disposes this instance and cancels all active subscriptions. + /// + /// The instance will be reset. Subsequent calls will instantiate a new one. void dispose() { for (final subscription in _subscriptions) { unawaited(subscription.cancel()); @@ -37,6 +48,9 @@ class GlobalPopups { instance = null; } + /// Registers the global backups to the given [context]. + /// + /// Subsequent calls will update the cached `context` but will not run the registration again. void register(final BuildContext context) { _context = context; if (_registered) { diff --git a/packages/neon/neon/lib/src/utils/hex_color.dart b/packages/neon/neon/lib/src/utils/hex_color.dart index 4ac99468..4452ce64 100644 --- a/packages/neon/neon/lib/src/utils/hex_color.dart +++ b/packages/neon/neon/lib/src/utils/hex_color.dart @@ -1,6 +1,8 @@ import 'dart:ui'; +/// A [Color] from a hex string. class HexColor extends Color { + /// Creates a new [Color] from the given [hexColor] string. HexColor(final String hexColor) : super(_parse(hexColor)); static int _parse(final String hexColor) { diff --git a/packages/neon/neon/lib/src/utils/request_manager.dart b/packages/neon/neon/lib/src/utils/request_manager.dart index 5222f823..7f8caf16 100644 --- a/packages/neon/neon/lib/src/utils/request_manager.dart +++ b/packages/neon/neon/lib/src/utils/request_manager.dart @@ -68,6 +68,7 @@ class RequestManager { disableTimeout, ); + @experimental Future wrapWebDav( final String clientID, final String k, diff --git a/packages/neon/neon/lib/src/utils/save_file.dart b/packages/neon/neon/lib/src/utils/save_file.dart index 36b27c83..9870ccce 100644 --- a/packages/neon/neon/lib/src/utils/save_file.dart +++ b/packages/neon/neon/lib/src/utils/save_file.dart @@ -5,6 +5,11 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:neon/src/platform/platform.dart'; +/// Displays a dialog for selecting a location where to save a file with the [data] content. +/// +/// Set the the suggested [fileName] to use when saving the file. +/// +/// Returns the path of the saved file or null if operation was cancelled. Future saveFileWithPickDialog(final String fileName, final Uint8List data) async { if (NeonPlatform.instance.shouldUseFileDialog) { // TODO: https://github.com/nextcloud/neon/issues/8 diff --git a/packages/neon/neon/lib/src/utils/validators.dart b/packages/neon/neon/lib/src/utils/validators.dart index 35942076..c3265824 100644 --- a/packages/neon/neon/lib/src/utils/validators.dart +++ b/packages/neon/neon/lib/src/utils/validators.dart @@ -1,6 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:neon/l10n/localizations.dart'; +/// Validates whether the given [input] is a valid http url. +/// +/// Set [httpsOnly] if you want to only allow http urls. +/// Returns null when the url is valid and a localized error message if not. String? validateHttpUrl( final BuildContext context, final String? input, { @@ -24,6 +28,9 @@ String? validateHttpUrl( return NeonLocalizations.of(context).errorInvalidURL; } +/// Validates that the given [input] is neither null nor empty. +/// +/// Returns null when not empty and a localized error message if empty. String? validateNotEmpty(final BuildContext context, final String? input) { if (input == null || input.isEmpty) { return NeonLocalizations.of(context).errorEmptyField; diff --git a/packages/neon/neon/lib/src/widgets/account_tile.dart b/packages/neon/neon/lib/src/widgets/account_tile.dart index 2a13d681..384811c9 100644 --- a/packages/neon/neon/lib/src/widgets/account_tile.dart +++ b/packages/neon/neon/lib/src/widgets/account_tile.dart @@ -10,8 +10,10 @@ import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/user_avatar.dart'; import 'package:nextcloud/provisioning_api.dart' as provisioning_api; +/// List tile to display account information. @internal class NeonAccountTile extends StatelessWidget { + /// Creates a new account list tile. const NeonAccountTile({ required this.account, this.trailing, @@ -20,9 +22,33 @@ class NeonAccountTile extends StatelessWidget { super.key, }); + /// {@template neon.AccountTile.account} + /// The account to display inside the tile. + /// {@endtemplate} final Account account; + + /// {@template neon.AccountTile.trailing} + /// A widget to display after the title. + /// + /// Typically an [Icon] widget. + /// + /// To show right-aligned metadata (assuming left-to-right reading order; + /// left-aligned for right-to-left reading order), consider using a [Row] with + /// [CrossAxisAlignment.baseline] alignment whose first item is [Expanded] and + /// whose second child is the metadata text, instead of using the [trailing] + /// property. + /// {@endtemplate} final Widget? trailing; + + /// {@template neon.AccountTile.onTap} + /// Called when the user taps this list tile. + /// {@endtemplate} final GestureTapCallback? onTap; + + /// Whether to also show the status on the avatar. + /// + /// See: + /// * [NeonUserAvatar.showStatus] final bool showStatus; @override diff --git a/packages/neon/neon/lib/src/widgets/app_bar.dart b/packages/neon/neon/lib/src/widgets/app_bar.dart index b76424c1..0b8fb5cb 100644 --- a/packages/neon/neon/lib/src/widgets/app_bar.dart +++ b/packages/neon/neon/lib/src/widgets/app_bar.dart @@ -17,8 +17,10 @@ import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; +/// Global app bar for the Neon app. @internal class NeonAppBar extends StatefulWidget implements PreferredSizeWidget { + /// Creates a new Neon app bar. const NeonAppBar({super.key}); @override @@ -149,8 +151,10 @@ class _NeonAppBarState extends State { ); } +/// Button opening the unified search page. @internal class SearchIconButton extends StatelessWidget { + /// Creates a new unified search button. const SearchIconButton({ super.key, }); @@ -167,8 +171,10 @@ class SearchIconButton extends StatelessWidget { ); } +/// Button opening the notifications page. @internal class NotificationIconButton extends StatefulWidget { + /// Creates a new notifications button. const NotificationIconButton({ super.key, }); diff --git a/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart b/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart index f8c83072..7ae07629 100644 --- a/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart +++ b/packages/neon/neon/lib/src/widgets/app_implementation_icon.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; import 'package:neon/src/models/app_implementation.dart'; +/// An icon widget displaying the app icon of a client with a overlay displaying the unreadCount. @internal class NeonAppImplementationIcon extends StatelessWidget { + /// Creates a new app const NeonAppImplementationIcon({ required this.appImplementation, this.unreadCount, @@ -12,12 +14,46 @@ class NeonAppImplementationIcon extends StatelessWidget { super.key, }); + /// The client for which to build the icon. + /// + /// Uses [AppImplementation.buildIcon] for the icon data. final AppImplementation appImplementation; + /// The number of unread notifications. + /// + /// use final int? unreadCount; + /// The color to use when drawing the icon. + /// + /// Defaults to the nearest [IconTheme]'s [IconThemeData.color]. + /// + /// The color (whether specified explicitly here or obtained from the + /// [IconTheme]) will be further adjusted by the nearest [IconTheme]'s + /// [IconThemeData.opacity]. + /// + /// {@tool snippet} + /// Typically, a Material Design color will be used, as follows: + /// + /// ```dart + /// Icon( + /// Icons.widgets, + /// color: Colors.blue.shade400, + /// ) + /// ``` + /// {@end-tool} final Color? color; + /// The size of the icon in logical pixels. + /// + /// Icons occupy a square with width and height equal to size. + /// + /// Defaults to the nearest [IconTheme]'s [IconThemeData.size]. + /// + /// If this [NeonAppImplementationIcon] is being placed inside an [IconButton], + /// then use [IconButton.iconSize] instead, so that the [IconButton] can make + /// the splash area the appropriate size as well. The [IconButton] uses an + /// [IconTheme] to pass down the size to the [NeonAppImplementationIcon]. final double? size; @override diff --git a/packages/neon/neon/lib/src/widgets/dialog.dart b/packages/neon/neon/lib/src/widgets/dialog.dart index 88dba87d..1c6be14a 100644 --- a/packages/neon/neon/lib/src/widgets/dialog.dart +++ b/packages/neon/neon/lib/src/widgets/dialog.dart @@ -1,13 +1,26 @@ import 'package:flutter/material.dart'; +/// A Neon material design dialog based on [SimpleDialog]. class NeonDialog extends StatelessWidget { + /// Creates a Neon dialog. + /// + /// Typically used in conjunction with [showDialog]. const NeonDialog({ this.title, this.children, super.key, }); + /// The (optional) title of the dialog is displayed in a large font at the top + /// of the dialog. + /// + /// Typically a [Text] widget. final Widget? title; + + /// The (optional) content of the dialog is displayed in a + /// [SingleChildScrollView] underneath the title. + /// + /// Typically a list of [SimpleDialogOption]s. final List? children; @override diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart index eb6b6c9a..aac07a83 100644 --- a/packages/neon/neon/lib/src/widgets/drawer.dart +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -15,8 +15,13 @@ import 'package:neon/src/widgets/image.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:nextcloud/core.dart' as core; +/// A custom pre populated [Drawer] side panel. +/// +/// Displays an entry for every registered and supported client and one for +/// the settings page. @internal class NeonDrawer extends StatelessWidget { + /// Created a new Neon drawer. const NeonDrawer({ super.key, }); @@ -112,8 +117,14 @@ class __NeonDrawerState extends State<_NeonDrawer> { } } +/// Custom styled [DrawerHeader] used inside a [Drawer] or [NeonDrawer]. +/// +/// The neon drawer will display the [core.ThemingPublicCapabilities_Theming.name] +/// and [core.ThemingPublicCapabilities_Theming.logo] and automatically rebuild +/// when the current theme changes. @internal class NeonDrawerHeader extends StatelessWidget { + /// Creates a new Neon drawer header. const NeonDrawerHeader({super.key}); @override diff --git a/packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart b/packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart index 60901b9b..e25d65c6 100644 --- a/packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart +++ b/packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +/// Wrapped [LinearProgressIndicator]. +/// +/// Adds default styling to the [LinearProgressIndicator]. class NeonLinearProgressIndicator extends StatelessWidget { + /// Creates a new Neon styled [LinearProgressIndicator]. const NeonLinearProgressIndicator({ this.visible = true, this.margin = const EdgeInsets.symmetric(horizontal: 10), @@ -9,9 +13,16 @@ class NeonLinearProgressIndicator extends StatelessWidget { super.key, }); + /// Whether the indicator is visible. final bool visible; + + /// Empty space to surround the indicator. final EdgeInsets? margin; + + /// {@macro flutter.progress_indicator.ProgressIndicator.color} final Color? color; + + /// {@macro flutter.material.LinearProgressIndicator.trackColor} final Color? backgroundColor; @override diff --git a/packages/neon/neon/lib/src/widgets/user_avatar.dart b/packages/neon/neon/lib/src/widgets/user_avatar.dart index c9877480..d7b6dc75 100644 --- a/packages/neon/neon/lib/src/widgets/user_avatar.dart +++ b/packages/neon/neon/lib/src/widgets/user_avatar.dart @@ -13,7 +13,9 @@ import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/user_status.dart' as user_status; import 'package:rxdart/rxdart.dart'; +/// A circle that contains the user profile image and status. class NeonUserAvatar extends StatefulWidget { + /// Creates a new Neon user avatar. NeonUserAvatar({ required this.account, final String? username, @@ -24,11 +26,23 @@ class NeonUserAvatar extends StatefulWidget { super.key, }) : username = username ?? account.username; + /// The account used to fetch the image. final Account account; + + /// The user profile to display final String username; + + /// Whether to also show the status. final bool showStatus; + + /// The size of the avatar. final double? size; + + /// The color with which to fill the circle. Changing the background + /// color will cause the avatar to animate to the new color. final Color? backgroundColor; + + /// The color used to render the loading animation. final Color? foregroundColor; @override diff --git a/packages/neon/neon/lib/src/widgets/validation_tile.dart b/packages/neon/neon/lib/src/widgets/validation_tile.dart index 62616db1..543688fe 100644 --- a/packages/neon/neon/lib/src/widgets/validation_tile.dart +++ b/packages/neon/neon/lib/src/widgets/validation_tile.dart @@ -1,13 +1,23 @@ import 'package:flutter/material.dart'; +/// Validation list tile. +/// +/// A [ListTile] used to display the progress of a validation. +/// +/// See: +/// * [ValidationState] for the possible states class NeonValidationTile extends StatelessWidget { + /// Creates a new validation list tile. const NeonValidationTile({ required this.title, required this.state, super.key, }); + /// The title of this tile. final String title; + + /// The state to display. final ValidationState state; @override @@ -48,9 +58,17 @@ class NeonValidationTile extends StatelessWidget { } } +/// Validation states for [NeonValidationTile]. enum ValidationState { + /// Indicates a loading state. loading, + + /// Indicates an error. failure, + + /// Indicates the process has been canceled. canceled, + + /// Indicates a success state. success, }