Browse Source

Merge pull request #674 from nextcloud/refactor/neon/settings

Refactor/neon/settings
pull/675/head
Nikolas Rimikis 1 year ago committed by GitHub
parent
commit
c724200fa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/neon/neon/lib/neon.dart
  2. 2
      packages/neon/neon/lib/settings.dart
  3. 32
      packages/neon/neon/lib/src/blocs/accounts.dart
  4. 11
      packages/neon/neon/lib/src/blocs/first_launch.dart
  5. 5
      packages/neon/neon/lib/src/blocs/push_notifications.dart
  6. 2
      packages/neon/neon/lib/src/models/app_implementation.dart
  7. 2
      packages/neon/neon/lib/src/settings/models/option.dart
  8. 8
      packages/neon/neon/lib/src/settings/models/select_option.dart
  9. 104
      packages/neon/neon/lib/src/settings/models/storage.dart
  10. 8
      packages/neon/neon/lib/src/settings/models/toggle_option.dart
  11. 11
      packages/neon/neon/lib/src/utils/account_options.dart
  12. 46
      packages/neon/neon/lib/src/utils/global_options.dart
  13. 11
      packages/neon/neon/lib/src/utils/push_utils.dart
  14. 2
      packages/neon/neon/lib/src/utils/settings_export_helper.dart
  15. 31
      packages/neon/neon/test/option_test.dart
  16. 119
      packages/neon/neon/test/storage_test.dart
  17. 29
      packages/neon/neon_files/lib/options.dart
  18. 41
      packages/neon/neon_news/lib/options.dart
  19. 26
      packages/neon/neon_notes/lib/options.dart

2
packages/neon/neon/lib/neon.dart

@ -31,7 +31,7 @@ Future runNeon({
await NeonPlatform.setup();
await RequestManager.instance.initCache();
await AppStorage.init();
await NeonStorage.init();
final packageInfo = await PackageInfo.fromPlatform();
buildUserAgent(packageInfo);

2
packages/neon/neon/lib/settings.dart

@ -2,6 +2,6 @@ export 'package:neon/src/models/label_builder.dart';
export 'package:neon/src/settings/models/nextcloud_app_options.dart';
export 'package:neon/src/settings/models/options_category.dart';
export 'package:neon/src/settings/models/select_option.dart';
export 'package:neon/src/settings/models/storage.dart';
export 'package:neon/src/settings/models/storage.dart' show Storable;
export 'package:neon/src/settings/models/toggle_option.dart';
export 'package:neon/src/settings/widgets/settings_list.dart';

32
packages/neon/neon/lib/src/blocs/accounts.dart

@ -58,24 +58,26 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
this._globalOptions,
this._allAppImplementations,
) {
const lastUsedStorage = SingleValueStorage(StorageKeys.lastUsedAccount);
accounts
..add(loadAccounts(_storage))
..add(loadAccounts())
..listen((final as) async {
_globalOptions.updateAccounts(as);
await _storage.setStringList(_keyAccounts, as.map((final a) => json.encode(a.toJson())).toList());
await saveAccounts(as);
});
activeAccount.listen((final aa) async {
if (aa != null) {
await _storage.setString(_keyLastUsedAccount, aa.id);
await lastUsedStorage.setString(aa.id);
} else {
await _storage.remove(_keyLastUsedAccount);
await lastUsedStorage.remove();
}
});
final as = accounts.value;
if (_globalOptions.rememberLastUsedAccount.value && _storage.containsKey(_keyLastUsedAccount)) {
final lastUsedAccountID = _storage.getString(_keyLastUsedAccount);
if (_globalOptions.rememberLastUsedAccount.value && lastUsedStorage.hasValue()) {
final lastUsedAccountID = lastUsedStorage.getString();
if (lastUsedAccountID != null) {
final aa = as.tryFind(lastUsedAccountID);
if (aa != null) {
@ -94,10 +96,8 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
}
}
late final AppStorage _storage = AppStorage('accounts');
final GlobalOptions _globalOptions;
final Iterable<AppImplementation> _allAppImplementations;
final _keyLastUsedAccount = 'last-used-account';
final _accountsOptions = <String, AccountSpecificOptions>{};
final _appsBlocs = <String, AppsBloc>{};
@ -210,7 +210,7 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
/// Use [activeOptions] to get them for the [activeAccount].
AccountSpecificOptions getOptionsFor(final Account account) =>
_accountsOptions[account.id] ??= AccountSpecificOptions(
AppStorage('accounts-${account.id}'),
AppStorage(StorageKeys.accounts, account.id),
getAppsBlocFor(account),
);
@ -277,10 +277,12 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
);
}
/// Get a list of logged in accounts from [storage].
/// Gets a list of logged in accounts from storage.
///
/// It is not checked whether the stored information is still valid.
List<Account> loadAccounts(final AppStorage storage) {
List<Account> loadAccounts() {
const storage = AppStorage(StorageKeys.accounts);
if (storage.containsKey(_keyAccounts)) {
return storage
.getStringList(_keyAccounts)!
@ -289,3 +291,11 @@ List<Account> loadAccounts(final AppStorage storage) {
}
return [];
}
/// Saves the given [accounts] to the storage.
Future<void> saveAccounts(final List<Account> accounts) async {
const storage = AppStorage(StorageKeys.accounts);
final values = accounts.map((final a) => json.encode(a.toJson())).toList();
await storage.setStringList(_keyAccounts, values);
}

11
packages/neon/neon/lib/src/blocs/first_launch.dart

@ -16,16 +16,15 @@ abstract class FirstLaunchBlocStates {
class FirstLaunchBloc extends Bloc implements FirstLaunchBlocEvents, FirstLaunchBlocStates {
FirstLaunchBloc({
final bool disabled = false,
}) : _storage = AppStorage(_keyFirstLaunch) {
if (!disabled && !_storage.containsKey(_keyFirstLaunch)) {
}) {
const storage = SingleValueStorage(StorageKeys.firstLaunch);
if (!disabled && !storage.hasValue()) {
onFirstLaunch.add(null);
unawaited(_storage.setBool(_keyFirstLaunch, false));
unawaited(storage.setBool(false));
}
}
final AppStorage _storage;
static const _keyFirstLaunch = 'first-launch';
@override
void dispose() {
unawaited(onFirstLaunch.close());

5
packages/neon/neon/lib/src/blocs/push_notifications.dart

@ -6,7 +6,6 @@ import 'package:meta/meta.dart';
import 'package:neon/src/bloc/bloc.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_ids.dart';
import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/settings/models/storage.dart';
@ -37,7 +36,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
}
final AccountsBloc _accountsBloc;
late final _storage = AppStorage(AppIDs.notifications);
late final _storage = const AppStorage(StorageKeys.notifications);
final GlobalOptions _globalOptions;
final _notificationsController = StreamController<PushNotification>();
@ -69,7 +68,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
Future _setupUnifiedPush() async {
// We just use a single RSA keypair for all accounts
final keypair = await PushUtils.loadRSAKeypair(_storage);
final keypair = await PushUtils.loadRSAKeypair();
await UnifiedPush.initialize(
onNewEndpoint: (final endpoint, final instance) async {

2
packages/neon/neon/lib/src/models/app_implementation.dart

@ -25,7 +25,7 @@ abstract class AppImplementation<T extends Bloc, R extends NextcloudAppOptions>
String name(final BuildContext context) => nameFromLocalization(AppLocalizations.of(context));
@protected
late final AppStorage storage = AppStorage('app-$id');
late final AppStorage storage = AppStorage(StorageKeys.apps, id);
@mustBeOverridden
R get options;

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

@ -38,7 +38,7 @@ abstract class Option<T> extends ChangeNotifier implements ValueListenable<T> {
}
final SettingsStorage storage;
final String key;
final Storable key;
final LabelBuilder label;
final T defaultValue;
final OptionsCategory? category;

8
packages/neon/neon/lib/src/settings/models/select_option.dart

@ -21,7 +21,7 @@ class SelectOption<T> extends Option<T> {
super.category,
super.enabled,
}) : _values = values,
super(initialValue: loadValue(values, storage.getString(key), forceLoad: forceLoadValue));
super(initialValue: loadValue(values, storage.getString(key.value), forceLoad: forceLoadValue));
/// Creates a SelectOption depending on the State of another [Option].
SelectOption.depend({
@ -39,7 +39,7 @@ class SelectOption<T> extends Option<T> {
final bool forceLoadValue = true,
super.category,
}) : _values = values,
super.depend(initialValue: loadValue(values, storage.getString(key), forceLoad: forceLoadValue));
super.depend(initialValue: loadValue(values, storage.getString(key.value), forceLoad: forceLoadValue));
static T? loadValue<T>(final Map<T, LabelBuilder> vs, final String? stored, {final bool forceLoad = true}) {
if (forceLoad && vs.isEmpty && stored is T) {
@ -51,7 +51,7 @@ class SelectOption<T> extends Option<T> {
@override
void reset() {
unawaited(storage.remove(key));
unawaited(storage.remove(key.value));
super.reset();
}
@ -63,7 +63,7 @@ class SelectOption<T> extends Option<T> {
super.value = value;
if (value != null) {
unawaited(storage.setString(key, serialize()!));
unawaited(storage.setString(key.value, serialize()!));
}
}

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

@ -1,6 +1,8 @@
import 'package:meta/meta.dart';
import 'package:neon/src/models/app_ids.dart';
import 'package:shared_preferences/shared_preferences.dart';
@internal
abstract interface class SettingsStorage {
String? getString(final String key);
@ -14,56 +16,126 @@ abstract interface class SettingsStorage {
Future<bool> remove(final String key);
}
class AppStorage implements SettingsStorage {
AppStorage(this._id);
abstract interface class Storable {
String get value;
}
@internal
enum StorageKeys implements Storable {
apps._('app'),
accounts._('accounts'),
global._('global'),
lastUsedAccount._('last-used-account'),
lastEndpoint._('last-endpoint'),
firstLaunch._('first-launch'),
notifications._(AppIDs.notifications);
final String _id;
const StorageKeys._(this.value);
@override
final String value;
}
@internal
final class NeonStorage {
/// Shared preferences instance.
///
/// Use [reqireDatabase] to access it.
/// Use [database] to access it.
/// Make sure it has been initialized wiht [init] before.
static SharedPreferences? _sharedPreferences;
@visibleForTesting
// ignore: use_setters_to_change_properties
static void mock(final SharedPreferences mock) => _sharedPreferences = mock;
/// Sets up the [SharedPreferences] instance.
///
/// Required to be called before accessing [reqireDatabase].
/// Required to be called before accessing [database].
static Future init() async {
if (_sharedPreferences != null) {
return;
}
_sharedPreferences = await SharedPreferences.getInstance();
}
@visibleForTesting
static SharedPreferences get reqireDatabase {
static SharedPreferences get database {
if (_sharedPreferences == null) {
throw StateError(
'AppStorage has not been initialized yet. Please make sure AppStorage.init() has been called before and completed.',
'NeonStorage has not been initialized yet. Please make sure NeonStorage.init() has been called before and completed.',
);
}
return _sharedPreferences!;
}
}
@immutable
@internal
final class SingleValueStorage {
const SingleValueStorage(this.key);
final StorageKeys key;
bool hasValue() => NeonStorage.database.containsKey(key.value);
Future<bool> remove() => NeonStorage.database.remove(key.value);
String? getString() => NeonStorage.database.getString(key.value);
String _formatKey(final String key) => '$_id-$key';
Future setString(final String value) => NeonStorage.database.setString(key.value, value);
bool? getBool() => NeonStorage.database.getBool(key.value);
// ignore: avoid_positional_boolean_parameters
Future setBool(final bool value) => NeonStorage.database.setBool(key.value, value);
List<String>? getStringList() => NeonStorage.database.getStringList(key.value);
Future setStringList(final List<String> value) => NeonStorage.database.setStringList(key.value, value);
}
@immutable
@internal
final class AppStorage implements SettingsStorage {
const AppStorage(
this.key, [
this.suffix,
]);
final StorageKeys key;
final String? suffix;
@visibleForTesting
String formatKey(final String key) {
if (suffix != null) {
return '${this.key.value}-$suffix-$key';
}
return '${this.key.value}-$key';
}
bool containsKey(final String key) => reqireDatabase.containsKey(_formatKey(key));
bool containsKey(final String key) => NeonStorage.database.containsKey(formatKey(key));
@override
Future<bool> remove(final String key) => reqireDatabase.remove(_formatKey(key));
Future<bool> remove(final String key) => NeonStorage.database.remove(formatKey(key));
@override
String? getString(final String key) => reqireDatabase.getString(_formatKey(key));
String? getString(final String key) => NeonStorage.database.getString(formatKey(key));
@override
Future setString(final String key, final String value) => reqireDatabase.setString(_formatKey(key), value);
Future setString(final String key, final String value) => NeonStorage.database.setString(formatKey(key), value);
@override
bool? getBool(final String key) => reqireDatabase.getBool(_formatKey(key));
bool? getBool(final String key) => NeonStorage.database.getBool(formatKey(key));
@override
Future setBool(final String key, final bool value) => reqireDatabase.setBool(_formatKey(key), value);
Future setBool(final String key, final bool value) => NeonStorage.database.setBool(formatKey(key), value);
List<String>? getStringList(final String key) => reqireDatabase.getStringList(_formatKey(key));
List<String>? getStringList(final String key) => NeonStorage.database.getStringList(formatKey(key));
Future setStringList(final String key, final List<String> value) =>
reqireDatabase.setStringList(_formatKey(key), value);
NeonStorage.database.setStringList(formatKey(key), value);
}

8
packages/neon/neon/lib/src/settings/models/toggle_option.dart

@ -11,7 +11,7 @@ class ToggleOption extends Option<bool> {
required final bool defaultValue,
super.category,
super.enabled,
}) : super(defaultValue: storage.getBool(key) ?? defaultValue);
}) : super(defaultValue: storage.getBool(key.value) ?? defaultValue);
/// Creates a ToggleOption depending on the State of another [Option].
ToggleOption.depend({
@ -22,12 +22,12 @@ class ToggleOption extends Option<bool> {
required super.enabled,
super.category,
}) : super.depend(
defaultValue: storage.getBool(key) ?? defaultValue,
defaultValue: storage.getBool(key.value) ?? defaultValue,
);
@override
void reset() {
unawaited(storage.remove(key));
unawaited(storage.remove(key.value));
super.reset();
}
@ -36,7 +36,7 @@ class ToggleOption extends Option<bool> {
set value(final bool value) {
super.value = value;
unawaited(storage.setBool(key, serialize()));
unawaited(storage.setBool(key.value, serialize()));
}
@override

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

@ -47,9 +47,18 @@ class AccountSpecificOptions {
late final initialApp = SelectOption<String?>(
storage: _storage,
key: 'initial-app',
key: AccountOptionKeys.initialApp,
label: (final context) => AppLocalizations.of(context).accountOptionsInitialApp,
defaultValue: null,
values: {},
);
}
enum AccountOptionKeys implements Storable {
initialApp._('initial-app');
const AccountOptionKeys._(this.value);
@override
final String value;
}

46
packages/neon/neon/lib/src/utils/global_options.dart

@ -49,7 +49,7 @@ class GlobalOptions {
}
}
late final AppStorage _storage = AppStorage('global');
late final AppStorage _storage = const AppStorage(StorageKeys.global);
final PackageInfo _packageInfo;
late final _distributorsMap = <String, String Function(BuildContext)>{
@ -125,7 +125,7 @@ class GlobalOptions {
late final themeMode = SelectOption<ThemeMode>(
storage: _storage,
key: 'theme-mode',
key: GlobalOptionKeys.themeMode,
label: (final context) => AppLocalizations.of(context).globalOptionsThemeMode,
defaultValue: ThemeMode.system,
values: {
@ -137,28 +137,28 @@ class GlobalOptions {
late final themeOLEDAsDark = ToggleOption(
storage: _storage,
key: 'theme-oled-as-dark',
key: GlobalOptionKeys.themeOledAsDark,
label: (final context) => AppLocalizations.of(context).globalOptionsThemeOLEDAsDark,
defaultValue: false,
);
late final themeKeepOriginalAccentColor = ToggleOption(
storage: _storage,
key: 'theme-keep-original-accent-color',
key: GlobalOptionKeys.themeKeepOriginalAccentColor,
label: (final context) => AppLocalizations.of(context).globalOptionsThemeKeepOriginalAccentColor,
defaultValue: false,
);
late final pushNotificationsEnabled = ToggleOption(
storage: _storage,
key: 'push-notifications-enabled',
key: GlobalOptionKeys.pushNotificationsEnabled,
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled,
defaultValue: false,
);
late final pushNotificationsDistributor = SelectOption<String?>.depend(
storage: _storage,
key: 'push-notifications-distributor',
key: GlobalOptionKeys.pushNotificationsDistributor,
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsDistributor,
defaultValue: null,
values: {},
@ -167,14 +167,14 @@ class GlobalOptions {
late final startupMinimized = ToggleOption(
storage: _storage,
key: 'startup-minimized',
key: GlobalOptionKeys.startupMinimized,
label: (final context) => AppLocalizations.of(context).globalOptionsStartupMinimized,
defaultValue: false,
);
late final startupMinimizeInsteadOfExit = ToggleOption(
storage: _storage,
key: 'startup-minimize-instead-of-exit',
key: GlobalOptionKeys.startupMinimizeInsteadOfExit,
label: (final context) => AppLocalizations.of(context).globalOptionsStartupMinimizeInsteadOfExit,
defaultValue: false,
);
@ -183,14 +183,14 @@ class GlobalOptions {
late final systemTrayEnabled = ToggleOption(
storage: _storage,
key: 'systemtray-enabled',
key: GlobalOptionKeys.systemtrayEnabled,
label: (final context) => AppLocalizations.of(context).globalOptionsSystemTrayEnabled,
defaultValue: false,
);
late final systemTrayHideToTrayWhenMinimized = ToggleOption.depend(
storage: _storage,
key: 'systemtray-hide-to-tray-when-minimized',
key: GlobalOptionKeys.systemtrayHideToTrayWhenMinimized,
label: (final context) => AppLocalizations.of(context).globalOptionsSystemTrayHideToTrayWhenMinimized,
defaultValue: true,
enabled: systemTrayEnabled,
@ -198,14 +198,14 @@ class GlobalOptions {
late final rememberLastUsedAccount = ToggleOption(
storage: _storage,
key: 'remember-last-used-account',
key: GlobalOptionKeys.rememberLastUsedAccount,
label: (final context) => AppLocalizations.of(context).globalOptionsAccountsRememberLastUsedAccount,
defaultValue: true,
);
late final initialAccount = SelectOption<String?>(
storage: _storage,
key: 'initial-account',
key: GlobalOptionKeys.initialAccount,
label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount,
defaultValue: null,
values: {},
@ -213,7 +213,7 @@ class GlobalOptions {
late final navigationMode = SelectOption<NavigationMode>(
storage: _storage,
key: 'navigation-mode',
key: GlobalOptionKeys.navigationMode,
label: (final context) => AppLocalizations.of(context).globalOptionsNavigationMode,
defaultValue: Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible,
values: {
@ -228,6 +228,26 @@ class GlobalOptions {
);
}
enum GlobalOptionKeys implements Storable {
themeMode._('theme-mode'),
themeOledAsDark._('theme-oled-as-dark'),
themeKeepOriginalAccentColor._('theme-keep-original-accent-color'),
pushNotificationsEnabled._('push-notifications-enabled'),
pushNotificationsDistributor._('push-notifications-distributor'),
startupMinimized._('startup-minimized'),
startupMinimizeInsteadOfExit._('startup-minimize-instead-of-exit'),
systemtrayEnabled._('systemtray-enabled'),
systemtrayHideToTrayWhenMinimized._('systemtray-hide-to-tray-when-minimized'),
rememberLastUsedAccount._('remember-last-used-account'),
initialAccount._('initial-account'),
navigationMode._('navigation-mode');
const GlobalOptionKeys._(this.value);
@override
final String value;
}
enum NavigationMode {
drawer,
drawerAlwaysVisible,

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

@ -11,7 +11,6 @@ import 'package:image/image.dart' as img;
import 'package:meta/meta.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_ids.dart';
import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/theme/colors.dart';
@ -22,7 +21,8 @@ import 'package:nextcloud/nextcloud.dart';
@internal
@immutable
class PushUtils {
static Future<RSAKeypair> loadRSAKeypair(final AppStorage storage) async {
static Future<RSAKeypair> loadRSAKeypair() async {
const storage = AppStorage(StorageKeys.notifications);
const keyDevicePrivateKey = 'device-private-key';
late RSAKeypair keypair;
@ -70,9 +70,9 @@ class PushUtils {
}
},
);
await AppStorage.init();
await NeonStorage.init();
final keypair = await loadRSAKeypair(AppStorage(AppIDs.notifications));
final keypair = await loadRSAKeypair();
for (final message in Uri(query: utf8.decode(messages)).queryParameters.values) {
final data = json.decode(message) as Map<String, dynamic>;
@ -93,12 +93,11 @@ class PushUtils {
} else {
final localizations = await appLocalizationsFromSystem();
var accounts = <Account>[];
final accounts = loadAccounts();
Account? account;
NotificationsNotification? notification;
AndroidBitmap<Object>? largeIconBitmap;
try {
accounts = loadAccounts(AppStorage('accounts'));
account = accounts.tryFind(instance);
if (account != null) {
notification =

2
packages/neon/neon/lib/src/utils/settings_export_helper.dart

@ -53,7 +53,7 @@ class SettingsExportHelper {
Future _applyOptionsMapToOptions(final List<Option> options, final Map<String, dynamic> data) async {
for (final optionKey in data.keys) {
for (final option in options) {
if (option.key == optionKey) {
if (option.key.value == optionKey) {
final Object? value = data[optionKey];
if (value != null) {

31
packages/neon/neon/test/option_test.dart

@ -15,6 +15,15 @@ class MockCallbackFunction extends Mock {
FutureOr<void> call();
}
enum StorageKey implements Storable {
key._('storage-key');
const StorageKey._(this.value);
@override
final String value;
}
enum SelectValues {
first,
second,
@ -23,7 +32,7 @@ enum SelectValues {
void main() {
final storage = MockStorage();
const key = 'storage-key';
const key = StorageKey.key;
String labelBuilder(final _) => 'label';
group('SelectOption', () {
@ -36,8 +45,8 @@ void main() {
late SelectOption<SelectValues> option;
setUp(() {
when(() => storage.setString(key, any())).thenAnswer((final _) async {});
when(() => storage.remove(key)).thenAnswer((final _) async => true);
when(() => storage.setString(key.value, any())).thenAnswer((final _) async {});
when(() => storage.remove(key.value)).thenAnswer((final _) async => true);
option = SelectOption<SelectValues>(
storage: storage,
@ -56,7 +65,7 @@ void main() {
test('Create', () {
expect(option.value, option.defaultValue, reason: 'Should default to defaultValue.');
when(() => storage.getString(key)).thenReturn('SelectValues.second');
when(() => storage.getString(key.value)).thenReturn('SelectValues.second');
option = SelectOption<SelectValues>(
storage: storage,
@ -96,7 +105,7 @@ void main() {
..value = SelectValues.third;
verify(callback.call).called(1);
verify(() => storage.setString(key, 'SelectValues.third')).called(1);
verify(() => storage.setString(key.value, 'SelectValues.third')).called(1);
expect(option.value, SelectValues.third, reason: 'Should update the value.');
option.value = SelectValues.third;
@ -150,7 +159,7 @@ void main() {
option.reset();
verify(callback.call).called(1);
verify(() => storage.remove(key)).called(1);
verify(() => storage.remove(key.value)).called(1);
expect(option.value, option.defaultValue, reason: 'Should reset the value.');
});
@ -171,8 +180,8 @@ void main() {
late ToggleOption option;
setUp(() {
when(() => storage.setBool(key, any())).thenAnswer((final _) async {});
when(() => storage.remove(key)).thenAnswer((final _) async => true);
when(() => storage.setBool(key.value, any())).thenAnswer((final _) async {});
when(() => storage.remove(key.value)).thenAnswer((final _) async => true);
option = ToggleOption(
storage: storage,
@ -190,7 +199,7 @@ void main() {
test('Create', () {
expect(option.value, option.defaultValue, reason: 'Should default to defaultValue.');
when(() => storage.getBool(key)).thenReturn(true);
when(() => storage.getBool(key.value)).thenReturn(true);
option = ToggleOption(
storage: storage,
@ -228,7 +237,7 @@ void main() {
..value = false;
verify(callback.call).called(1);
verify(() => storage.setBool(key, false)).called(1);
verify(() => storage.setBool(key.value, false)).called(1);
expect(option.value, false, reason: 'Should update the value.');
option.value = false;
@ -262,7 +271,7 @@ void main() {
option.reset();
verify(callback.call).called(1);
verify(() => storage.remove(key)).called(1);
verify(() => storage.remove(key.value)).called(1);
expect(option.value, option.defaultValue, reason: 'Should reset the value.');
});
});

119
packages/neon/neon/test/storage_test.dart

@ -1,14 +1,125 @@
import 'package:mocktail/mocktail.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:test/test.dart';
class SharedPreferencesMock extends Mock implements SharedPreferences {}
void main() {
test('AppStorage', () async {
expect(() => AppStorage.reqireDatabase, throwsA(isA<StateError>()));
test('NeonStorage', () async {
expect(() => NeonStorage.database, throwsA(isA<StateError>()));
SharedPreferences.setMockInitialValues({});
await AppStorage.init();
await NeonStorage.init();
expect(NeonStorage.database, isA<SharedPreferences>());
});
group('AppStorage', () {
test('formatKey', () async {
var appStorage = const AppStorage(StorageKeys.accounts);
var key = appStorage.formatKey('test-key');
expect(key, 'accounts-test-key');
appStorage = const AppStorage(StorageKeys.accounts, 'test-suffix');
key = appStorage.formatKey('test-key');
expect(key, 'accounts-test-suffix-test-key');
});
test('interface', () async {
final sharedPreferences = SharedPreferencesMock();
NeonStorage.mock(sharedPreferences);
const appStorage = AppStorage(StorageKeys.accounts);
const key = 'key';
final formatedKey = appStorage.formatKey(key);
when(() => sharedPreferences.containsKey(formatedKey)).thenReturn(true);
dynamic result = appStorage.containsKey(key);
expect(result, equals(true));
verify(() => sharedPreferences.containsKey(formatedKey)).called(1);
when(() => sharedPreferences.remove(formatedKey)).thenAnswer((final _) => Future.value(false));
result = await appStorage.remove(key);
expect(result, equals(false));
verify(() => sharedPreferences.remove(formatedKey)).called(1);
when(() => sharedPreferences.getString(formatedKey)).thenReturn(null);
result = appStorage.getString(key);
expect(result, isNull);
verify(() => sharedPreferences.getString(formatedKey)).called(1);
when(() => sharedPreferences.setString(formatedKey, 'value')).thenAnswer((final _) => Future.value(false));
result = await appStorage.setString(key, 'value');
expect(result, false);
verify(() => sharedPreferences.setString(formatedKey, 'value')).called(1);
when(() => sharedPreferences.getBool(formatedKey)).thenReturn(true);
result = appStorage.getBool(key);
expect(result, equals(true));
verify(() => sharedPreferences.getBool(formatedKey)).called(1);
when(() => sharedPreferences.setBool(formatedKey, true)).thenAnswer((final _) => Future.value(true));
result = await appStorage.setBool(key, true);
expect(result, true);
verify(() => sharedPreferences.setBool(formatedKey, true)).called(1);
when(() => sharedPreferences.getStringList(formatedKey)).thenReturn(['hi there']);
result = appStorage.getStringList(key);
expect(result, equals(['hi there']));
verify(() => sharedPreferences.getStringList(formatedKey)).called(1);
when(() => sharedPreferences.setStringList(formatedKey, ['hi there']))
.thenAnswer((final _) => Future.value(false));
result = await appStorage.setStringList(key, ['hi there']);
expect(result, false);
verify(() => sharedPreferences.setStringList(formatedKey, ['hi there'])).called(1);
});
});
test('SingleValueStorage', () async {
final sharedPreferences = SharedPreferencesMock();
NeonStorage.mock(sharedPreferences);
const storage = SingleValueStorage(StorageKeys.global);
final key = StorageKeys.global.value;
when(() => sharedPreferences.containsKey(key)).thenReturn(true);
dynamic result = storage.hasValue();
expect(result, equals(true));
verify(() => sharedPreferences.containsKey(key)).called(1);
when(() => sharedPreferences.remove(key)).thenAnswer((final _) => Future.value(false));
result = await storage.remove();
expect(result, equals(false));
verify(() => sharedPreferences.remove(key)).called(1);
when(() => sharedPreferences.getString(key)).thenReturn(null);
result = storage.getString();
expect(result, isNull);
verify(() => sharedPreferences.getString(key)).called(1);
when(() => sharedPreferences.setString(key, 'value')).thenAnswer((final _) => Future.value(false));
result = await storage.setString('value');
expect(result, false);
verify(() => sharedPreferences.setString(key, 'value')).called(1);
when(() => sharedPreferences.getBool(key)).thenReturn(true);
result = storage.getBool();
expect(result, equals(true));
verify(() => sharedPreferences.getBool(key)).called(1);
when(() => sharedPreferences.setBool(key, true)).thenAnswer((final _) => Future.value(true));
result = await storage.setBool(true);
expect(result, true);
verify(() => sharedPreferences.setBool(key, true)).called(1);
when(() => sharedPreferences.getStringList(key)).thenReturn(['hi there']);
result = storage.getStringList();
expect(result, equals(['hi there']));
verify(() => sharedPreferences.getStringList(key)).called(1);
expect(AppStorage.reqireDatabase, isA<SharedPreferences>());
when(() => sharedPreferences.setStringList(key, ['hi there'])).thenAnswer((final _) => Future.value(false));
result = await storage.setStringList(['hi there']);
expect(result, false);
verify(() => sharedPreferences.setStringList(key, ['hi there'])).called(1);
});
}

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

@ -23,7 +23,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final filesSortPropertyOption = SelectOption<FilesSortProperty>(
storage: super.storage,
category: generalCategory,
key: 'files-sort-property',
key: FilesOptionKeys.sortProperty,
label: (final context) => AppLocalizations.of(context).optionsFilesSortProperty,
defaultValue: FilesSortProperty.name,
values: {
@ -37,7 +37,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final filesSortBoxOrderOption = SelectOption<SortBoxOrder>(
storage: super.storage,
category: generalCategory,
key: 'files-sort-box-order',
key: FilesOptionKeys.sortOrder,
label: (final context) => AppLocalizations.of(context).optionsFilesSortOrder,
defaultValue: SortBoxOrder.ascending,
values: sortBoxOrderOptionValues,
@ -46,7 +46,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final showPreviewsOption = ToggleOption(
storage: super.storage,
category: generalCategory,
key: 'show-previews',
key: FilesOptionKeys.showPreviews,
label: (final context) => AppLocalizations.of(context).optionsShowPreviews,
defaultValue: true,
);
@ -54,7 +54,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final uploadQueueParallelism = SelectOption<int>(
storage: storage,
category: generalCategory,
key: 'upload-queue-parallelism',
key: FilesOptionKeys.uploadQueueParallelism,
label: (final context) => AppLocalizations.of(context).optionsUploadQueueParallelism,
defaultValue: 4,
values: {
@ -67,7 +67,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final downloadQueueParallelism = SelectOption<int>(
storage: storage,
category: generalCategory,
key: 'download-queue-parallelism',
key: FilesOptionKeys.downloadQueueParallelism,
label: (final context) => AppLocalizations.of(context).optionsDownloadQueueParallelism,
defaultValue: 4,
values: {
@ -97,7 +97,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final uploadSizeWarning = SelectOption<int?>(
storage: storage,
category: generalCategory,
key: 'upload-size-warning',
key: FilesOptionKeys.uploadQueueParallelism,
label: (final context) => AppLocalizations.of(context).optionsUploadSizeWarning,
defaultValue: _mb(10),
values: _sizeWarningValues,
@ -106,13 +106,28 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
late final downloadSizeWarning = SelectOption<int?>(
storage: storage,
category: generalCategory,
key: 'download-size-warning',
key: FilesOptionKeys.downloadSizeWarning,
label: (final context) => AppLocalizations.of(context).optionsDownloadSizeWarning,
defaultValue: _mb(10),
values: _sizeWarningValues,
);
}
enum FilesOptionKeys implements Storable {
sortProperty._('files-sort-property'),
sortOrder._('files-sort-box-order'),
showPreviews._('show-previews'),
uploadQueueParallelism._('upload-queue-parallelism'),
downloadQueueParallelism._('download-queue-parallelism'),
uploadSizeWarning._('upload-size-warning'),
downloadSizeWarning._('download-size-warning');
const FilesOptionKeys._(this.value);
@override
final String value;
}
enum FilesSortProperty {
name,
modifiedDate,

41
packages/neon/neon_news/lib/options.dart

@ -42,7 +42,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final defaultCategoryOption = SelectOption<DefaultCategory>(
storage: super.storage,
category: generalCategory,
key: 'default-category',
key: NewsOptionKeys.defaultCategory,
label: (final context) => AppLocalizations.of(context).optionsDefaultCategory,
defaultValue: DefaultCategory.articles,
values: {
@ -55,7 +55,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final articleViewTypeOption = SelectOption<ArticleViewType>(
storage: super.storage,
category: articlesCategory,
key: 'article-view-type',
key: NewsOptionKeys.articleViewType,
label: (final context) => AppLocalizations.of(context).optionsArticleViewType,
defaultValue: ArticleViewType.direct,
values: {
@ -71,7 +71,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final articleDisableMarkAsReadTimeoutOption = ToggleOption(
storage: super.storage,
category: articlesCategory,
key: 'article-disable-mark-as-read-timeout',
key: NewsOptionKeys.articleDisableMarkAsReadTimeout,
label: (final context) => AppLocalizations.of(context).optionsArticleDisableMarkAsReadTimeout,
defaultValue: false,
);
@ -79,7 +79,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final defaultArticlesFilterOption = SelectOption<FilterType>(
storage: super.storage,
category: articlesCategory,
key: 'default-articles-filter',
key: NewsOptionKeys.defaultArticlesFilter,
label: (final context) => AppLocalizations.of(context).optionsDefaultArticlesFilter,
defaultValue: FilterType.unread,
values: {
@ -92,7 +92,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final articlesSortPropertyOption = SelectOption<ArticlesSortProperty>(
storage: super.storage,
category: articlesCategory,
key: 'articles-sort-property',
key: NewsOptionKeys.articlesSortProperty,
label: (final context) => AppLocalizations.of(context).optionsArticlesSortProperty,
defaultValue: ArticlesSortProperty.publishDate,
values: {
@ -107,7 +107,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final articlesSortBoxOrderOption = SelectOption<SortBoxOrder>(
storage: super.storage,
category: articlesCategory,
key: 'articles-sort-box-order',
key: NewsOptionKeys.articlesSortBoxOrder,
label: (final context) => AppLocalizations.of(context).optionsArticlesSortOrder,
defaultValue: SortBoxOrder.descending,
values: sortBoxOrderOptionValues,
@ -116,7 +116,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final foldersSortPropertyOption = SelectOption<FoldersSortProperty>(
storage: super.storage,
category: foldersCategory,
key: 'folders-sort-property',
key: NewsOptionKeys.foldersSortProperty,
label: (final context) => AppLocalizations.of(context).optionsFoldersSortProperty,
defaultValue: FoldersSortProperty.alphabetical,
values: {
@ -130,7 +130,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final foldersSortBoxOrderOption = SelectOption<SortBoxOrder>(
storage: super.storage,
category: foldersCategory,
key: 'folders-sort-box-order',
key: NewsOptionKeys.foldersSortBoxOrder,
label: (final context) => AppLocalizations.of(context).optionsFoldersSortOrder,
defaultValue: SortBoxOrder.ascending,
values: sortBoxOrderOptionValues,
@ -139,7 +139,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final defaultFolderViewTypeOption = SelectOption<DefaultFolderViewType>(
storage: super.storage,
category: foldersCategory,
key: 'default-folder-view-type',
key: NewsOptionKeys.defaultFolderViewType,
label: (final context) => AppLocalizations.of(context).optionsDefaultFolderViewType,
defaultValue: DefaultFolderViewType.articles,
values: {
@ -151,7 +151,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final feedsSortPropertyOption = SelectOption<FeedsSortProperty>(
storage: super.storage,
category: feedsCategory,
key: 'feeds-sort-property',
key: NewsOptionKeys.feedsSortProperty,
label: (final context) => AppLocalizations.of(context).optionsFeedsSortProperty,
defaultValue: FeedsSortProperty.alphabetical,
values: {
@ -165,13 +165,32 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
late final feedsSortBoxOrderOption = SelectOption<SortBoxOrder>(
storage: super.storage,
category: feedsCategory,
key: 'feeds-sort-box-order',
key: NewsOptionKeys.feedsSortBoxOrder,
label: (final context) => AppLocalizations.of(context).optionsFeedsSortOrder,
defaultValue: SortBoxOrder.ascending,
values: sortBoxOrderOptionValues,
);
}
enum NewsOptionKeys implements Storable {
defaultCategory._('default-category'),
articleViewType._('article-view-type'),
articleDisableMarkAsReadTimeout._('article-disable-mark-as-read-timeout'),
defaultArticlesFilter._('default-articles-filter'),
articlesSortProperty._('articles-sort-property'),
articlesSortBoxOrder._('articles-sort-box-order'),
foldersSortProperty._('folders-sort-property'),
foldersSortBoxOrder._('folders-sort-box-order'),
defaultFolderViewType._('default-folder-view-type'),
feedsSortProperty._('feeds-sort-property'),
feedsSortBoxOrder._('feeds-sort-box-order');
const NewsOptionKeys._(this.value);
@override
final String value;
}
enum DefaultCategory {
articles,
folders,

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

@ -32,7 +32,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
late final defaultCategoryOption = SelectOption<DefaultCategory>(
storage: super.storage,
category: generalCategory,
key: 'default-category',
key: NotesOptionKeys.defaultCategory,
label: (final context) => AppLocalizations.of(context).optionsDefaultCategory,
defaultValue: DefaultCategory.notes,
values: {
@ -44,7 +44,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
late final defaultNoteViewTypeOption = SelectOption<DefaultNoteViewType>(
storage: super.storage,
category: generalCategory,
key: 'default-note-view-type',
key: NotesOptionKeys.defaultNoteViewType,
label: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewType,
defaultValue: DefaultNoteViewType.preview,
values: {
@ -56,7 +56,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
late final notesSortPropertyOption = SelectOption<NotesSortProperty>(
storage: super.storage,
category: notesCategory,
key: 'notes-sort-property',
key: NotesOptionKeys.notesSortProperty,
label: (final context) => AppLocalizations.of(context).optionsNotesSortProperty,
defaultValue: NotesSortProperty.lastModified,
values: {
@ -70,7 +70,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
late final notesSortBoxOrderOption = SelectOption<SortBoxOrder>(
storage: super.storage,
category: notesCategory,
key: 'notes-sort-box-order',
key: NotesOptionKeys.notesSortBoxOrder,
label: (final context) => AppLocalizations.of(context).optionsNotesSortOrder,
defaultValue: SortBoxOrder.descending,
values: sortBoxOrderOptionValues,
@ -79,7 +79,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
late final categoriesSortPropertyOption = SelectOption<CategoriesSortProperty>(
storage: super.storage,
category: categoriesCategory,
key: 'categories-sort-property',
key: NotesOptionKeys.categoriesSortProperty,
label: (final context) => AppLocalizations.of(context).optionsCategoriesSortProperty,
defaultValue: CategoriesSortProperty.alphabetical,
values: {
@ -93,13 +93,27 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
late final categoriesSortBoxOrderOption = SelectOption<SortBoxOrder>(
storage: super.storage,
category: categoriesCategory,
key: 'categories-sort-box-order',
key: NotesOptionKeys.categoriesSortBoxOrder,
label: (final context) => AppLocalizations.of(context).optionsCategoriesSortOrder,
defaultValue: SortBoxOrder.ascending,
values: sortBoxOrderOptionValues,
);
}
enum NotesOptionKeys implements Storable {
defaultCategory._('default-category'),
defaultNoteViewType._('default-note-view-type'),
notesSortProperty._('notes-sort-property'),
notesSortBoxOrder._('notes-sort-box-order'),
categoriesSortProperty._('categories-sort-property'),
categoriesSortBoxOrder._('categories-sort-box-order');
const NotesOptionKeys._(this.value);
@override
final String value;
}
enum DefaultNoteViewType {
preview,
edit,

Loading…
Cancel
Save