diff --git a/packages/neon/neon/lib/src/settings/models/option.dart b/packages/neon/neon/lib/src/settings/models/option.dart index af78a9d0..54279ff9 100644 --- a/packages/neon/neon/lib/src/settings/models/option.dart +++ b/packages/neon/neon/lib/src/settings/models/option.dart @@ -97,7 +97,7 @@ sealed class Option extends ChangeNotifier implements ValueListenable, Dis notifyListeners(); } - /// Resets the option to its [default] value. + /// Resets the option to its [defaultValue] value. @mustBeOverridden void reset() { value = defaultValue; @@ -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..9dd1f394 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 `bool` [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 `String` [value]s 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