Browse Source

docs(neon): document public facing api

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/745/head
Nikolas Rimikis 2 years ago
parent
commit
75254df069
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 3
      packages/neon/neon/lib/neon.dart
  2. 8
      packages/neon/neon/lib/src/app.dart
  3. 25
      packages/neon/neon/lib/src/bloc/bloc.dart
  4. 7
      packages/neon/neon/lib/src/blocs/accounts.dart
  5. 15
      packages/neon/neon/lib/src/blocs/apps.dart
  6. 1
      packages/neon/neon/lib/src/blocs/capabilities.dart
  7. 21
      packages/neon/neon/lib/src/models/account.dart
  8. 1
      packages/neon/neon/lib/src/models/account_cache.dart
  9. 55
      packages/neon/neon/lib/src/models/app_implementation.dart
  10. 2
      packages/neon/neon/lib/src/models/disposable.dart
  11. 4
      packages/neon/neon/lib/src/models/label_builder.dart
  12. 11
      packages/neon/neon/lib/src/models/notifications_interface.dart
  13. 7
      packages/neon/neon/lib/src/pages/account_settings.dart
  14. 2
      packages/neon/neon/lib/src/pages/home.dart
  15. 31
      packages/neon/neon/lib/src/pages/settings.dart
  16. 7
      packages/neon/neon/lib/src/platform/android.dart
  17. 7
      packages/neon/neon/lib/src/platform/linux.dart
  18. 31
      packages/neon/neon/lib/src/platform/platform.dart
  19. 90
      packages/neon/neon/lib/src/router.dart
  20. 3
      packages/neon/neon/lib/src/settings/models/option.dart
  21. 4
      packages/neon/neon/lib/src/settings/models/options_category.dart
  22. 4
      packages/neon/neon/lib/src/settings/models/options_collection.dart
  23. 116
      packages/neon/neon/lib/src/settings/models/storage.dart
  24. 3
      packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart
  25. 6
      packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart
  26. 1
      packages/neon/neon/lib/src/theme/color_scheme.dart
  27. 18
      packages/neon/neon/lib/src/theme/theme.dart
  28. 7
      packages/neon/neon/lib/src/utils/account_options.dart
  29. 1
      packages/neon/neon/lib/src/utils/global.dart
  30. 91
      packages/neon/neon/lib/src/utils/global_options.dart
  31. 14
      packages/neon/neon/lib/src/utils/global_popups.dart
  32. 2
      packages/neon/neon/lib/src/utils/hex_color.dart
  33. 1
      packages/neon/neon/lib/src/utils/request_manager.dart
  34. 5
      packages/neon/neon/lib/src/utils/save_file.dart
  35. 7
      packages/neon/neon/lib/src/utils/validators.dart
  36. 26
      packages/neon/neon/lib/src/widgets/account_tile.dart
  37. 6
      packages/neon/neon/lib/src/widgets/app_bar.dart
  38. 36
      packages/neon/neon/lib/src/widgets/app_implementation_icon.dart
  39. 13
      packages/neon/neon/lib/src/widgets/dialog.dart
  40. 11
      packages/neon/neon/lib/src/widgets/drawer.dart
  41. 11
      packages/neon/neon/lib/src/widgets/linear_progress_indicator.dart
  42. 14
      packages/neon/neon/lib/src/widgets/user_avatar.dart
  43. 18
      packages/neon/neon/lib/src/widgets/validation_tile.dart

3
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<void> runNeon({
required final Set<AppImplementation> appImplementations,
required final NeonTheme theme,

8
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

25
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<Object>();
/// A stream of error events.
late Stream<Object> errors = _errorsStreamController.stream.asBroadcastStream();
/// Refreshes the state of the bloc.
///
/// Commonly involves re fetching data from the server.
FutureOr<void> 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, {

7
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<Account?> 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,

15
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<Result<Iterable<AppImplementation>>> get appImplementations;
/// The interface of the notifications app.
BehaviorSubject<Result<NotificationsAppInterface?>> get notificationsAppImplementation;
/// The currently active app.
BehaviorSubject<AppImplementation> get activeApp;
/// A subject emitting an event when the notifications page should be opened.
BehaviorSubject<void> get openNotifications;
/// A collection of unsupported apps and their minimum required version.
BehaviorSubject<Map<String, String?>> 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<T extends Bloc>(final AppImplementation<T, dynamic> appImplementation) =>
appImplementation.getBloc(_account);
/// Returns the active [Bloc] for every registered [AppImplementation] wrapped in a Provider.
List<Provider<Bloc>> get appBlocProviders =>
_allAppImplementations.map((final appImplementation) => appImplementation.blocProvider).toList();
}

1
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,
) {

21
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<String, dynamic> json) => _$AccountFromJson(json);
/// Parses this object into a json like map.
Map<String, dynamic> 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<String, String> _idCache = {};
/// Extension to find an account by id in a Iterable.
extension AccountFind on Iterable<Account> {
/// 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);
}

1
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<T extends Disposable> implements Disposable {
/// Creates a new account cache.
AccountCache();
final Map<String, T> _cache = {};

55
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<T extends Bloc, R extends NextcloudAppOptions> 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<Object> get localizationsDelegate;
/// {@macro flutter.widgets.widgetsApp.supportedLocales}
Iterable<Locale> 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<T extends Bloc, R extends NextcloudAppOptions>
final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities,
);
/// Cache for all blocs.
///
/// To access a bloc use [getBloc] instead.
final blocsCache = AccountCache<T>();
/// 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<T> get blocProvider => Provider<T>(
create: (final context) {
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
@ -62,10 +95,17 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
},
);
/// The count of unread notifications.
///
/// If null no label will be displayed.
BehaviorSubject<int>? 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<AccountsBloc>(context);
final account = accountsBloc.activeAccount.value!;
@ -89,7 +129,7 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
/// 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<T extends Bloc, R extends NextcloudAppOptions>
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<T extends Bloc, R extends NextcloudAppOptions>
int get hashCode => id.hashCode;
}
/// Extension to find an app implementation by id in a Iterable.
extension AppImplementationFind on Iterable<AppImplementation> {
/// 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);
}

2
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<Disposable> {
/// Calls [Disposable.dispose] on all entries.
///
@ -22,6 +23,7 @@ extension DisposableIterableBloc on Iterable<Disposable> {
}
}
/// Extension on [Disposable] maps.
extension DisposableMapBloc on Map<dynamic, Disposable> {
/// Calls [Disposable.dispose] on all entries.
///

4
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);

11
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<T extends NotificationsBlocInterface,
R extends NotificationsOptionsInterface> extends AppImplementation<T, R> {
/// Creates a new notifications client.
NotificationsAppInterface();
@override
@ -12,13 +16,20 @@ abstract interface class NotificationsAppInterface<T extends NotificationsBlocIn
R get options => 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);
}

7
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

2
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,
});

31
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

7
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

7
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

31
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<String> get userAccessibleAppDataPath;
/// Initializes this platform.
FutureOr<void> init();
}

90
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<NavigatorState> navigatorKey,
@ -68,12 +69,19 @@ Page<void> _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<HomeRoute>(
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<LoginRoute>(
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;

3
packages/neon/neon/lib/src/settings/models/option.dart

@ -216,6 +216,9 @@ class SelectOption<T> extends Option<T> {
/// * [value] for the currently selected one
Map<T, LabelBuilder> 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<T, LabelBuilder> newValues) {
if (_values == newValues) {
return;

4
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;
}

4
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.

116
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<bool> 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<bool> setBool(final String key, final bool value);
/// {@template NeonStorage.remove}
/// Removes an entry from persistent storage.
/// {@endtemplate}
Future<bool> 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<bool> remove() => NeonStorage.database.remove(key.value);
/// {@macro NeonStorage.getString}
String? getString() => NeonStorage.database.getString(key.value);
/// {@macro NeonStorage.setString}
Future<bool> 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<bool> setBool(final bool value) => NeonStorage.database.setBool(key.value, value);
/// {@macro NeonStorage.getStringList}
List<String>? getStringList() => NeonStorage.database.getStringList(key.value);
/// {@macro NeonStorage.setStringList}
Future<bool> setStringList(final List<String> 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<bool> 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<String>? 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<bool> setStringList(final String key, final List<String> value) =>
NeonStorage.database.setStringList(formatKey(key), value);
}

3
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.

6
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

1
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,
});

18
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<ThemeExtension>? 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(

7
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<String?>(
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);

1
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._();

91
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<Account> 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<String> 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<String?>.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<String?>(
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,
}

14
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 = <StreamSubscription<dynamic>>[];
/// 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) {

2
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) {

1
packages/neon/neon/lib/src/utils/request_manager.dart

@ -68,6 +68,7 @@ class RequestManager {
disableTimeout,
);
@experimental
Future<void> wrapWebDav<T>(
final String clientID,
final String k,

5
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<String?> saveFileWithPickDialog(final String fileName, final Uint8List data) async {
if (NeonPlatform.instance.shouldUseFileDialog) {
// TODO: https://github.com/nextcloud/neon/issues/8

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

26
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

6
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<NeonAppBar> {
);
}
/// 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,
});

36
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

13
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<Widget>? children;
@override

11
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

11
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

14
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

18
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,
}

Loading…
Cancel
Save