diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index 8b23e1b6..62d4e126 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/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); diff --git a/packages/neon/neon/lib/settings.dart b/packages/neon/neon/lib/settings.dart index 08c0053e..13193417 100644 --- a/packages/neon/neon/lib/settings.dart +++ b/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'; diff --git a/packages/neon/neon/lib/src/blocs/accounts.dart b/packages/neon/neon/lib/src/blocs/accounts.dart index 10605cea..a8ad92b1 100644 --- a/packages/neon/neon/lib/src/blocs/accounts.dart +++ b/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 _allAppImplementations; - final _keyLastUsedAccount = 'last-used-account'; final _accountsOptions = {}; final _appsBlocs = {}; @@ -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 loadAccounts(final AppStorage storage) { +List loadAccounts() { + const storage = AppStorage(StorageKeys.accounts); + if (storage.containsKey(_keyAccounts)) { return storage .getStringList(_keyAccounts)! @@ -289,3 +291,11 @@ List loadAccounts(final AppStorage storage) { } return []; } + +/// Saves the given [accounts] to the storage. +Future saveAccounts(final List accounts) async { + const storage = AppStorage(StorageKeys.accounts); + final values = accounts.map((final a) => json.encode(a.toJson())).toList(); + + await storage.setStringList(_keyAccounts, values); +} diff --git a/packages/neon/neon/lib/src/blocs/first_launch.dart b/packages/neon/neon/lib/src/blocs/first_launch.dart index fef5589c..056e9378 100644 --- a/packages/neon/neon/lib/src/blocs/first_launch.dart +++ b/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()); diff --git a/packages/neon/neon/lib/src/blocs/push_notifications.dart b/packages/neon/neon/lib/src/blocs/push_notifications.dart index c25a71f1..a1fde3fd 100644 --- a/packages/neon/neon/lib/src/blocs/push_notifications.dart +++ b/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(); @@ -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 { diff --git a/packages/neon/neon/lib/src/models/app_implementation.dart b/packages/neon/neon/lib/src/models/app_implementation.dart index 4628d10d..1051a571 100644 --- a/packages/neon/neon/lib/src/models/app_implementation.dart +++ b/packages/neon/neon/lib/src/models/app_implementation.dart @@ -25,7 +25,7 @@ abstract class AppImplementation 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; diff --git a/packages/neon/neon/lib/src/settings/models/option.dart b/packages/neon/neon/lib/src/settings/models/option.dart index 4d33bfb3..ac954b4d 100644 --- a/packages/neon/neon/lib/src/settings/models/option.dart +++ b/packages/neon/neon/lib/src/settings/models/option.dart @@ -38,7 +38,7 @@ abstract class Option extends ChangeNotifier implements ValueListenable { } final SettingsStorage storage; - final String key; + final Storable key; final LabelBuilder label; final T defaultValue; final OptionsCategory? category; diff --git a/packages/neon/neon/lib/src/settings/models/select_option.dart b/packages/neon/neon/lib/src/settings/models/select_option.dart index 0cc0b9e2..6139cbe5 100644 --- a/packages/neon/neon/lib/src/settings/models/select_option.dart +++ b/packages/neon/neon/lib/src/settings/models/select_option.dart @@ -21,7 +21,7 @@ class SelectOption extends Option { 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 extends Option { 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(final Map vs, final String? stored, {final bool forceLoad = true}) { if (forceLoad && vs.isEmpty && stored is T) { @@ -51,7 +51,7 @@ class SelectOption extends Option { @override void reset() { - unawaited(storage.remove(key)); + unawaited(storage.remove(key.value)); super.reset(); } @@ -63,7 +63,7 @@ class SelectOption extends Option { super.value = value; if (value != null) { - unawaited(storage.setString(key, serialize()!)); + unawaited(storage.setString(key.value, serialize()!)); } } diff --git a/packages/neon/neon/lib/src/settings/models/storage.dart b/packages/neon/neon/lib/src/settings/models/storage.dart index d033964d..ebdd58ee 100644 --- a/packages/neon/neon/lib/src/settings/models/storage.dart +++ b/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 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 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? getStringList() => NeonStorage.database.getStringList(key.value); + + Future setStringList(final List 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 remove(final String key) => reqireDatabase.remove(_formatKey(key)); + Future 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? getStringList(final String key) => reqireDatabase.getStringList(_formatKey(key)); + List? getStringList(final String key) => NeonStorage.database.getStringList(formatKey(key)); Future setStringList(final String key, final List value) => - reqireDatabase.setStringList(_formatKey(key), value); + NeonStorage.database.setStringList(formatKey(key), value); } diff --git a/packages/neon/neon/lib/src/settings/models/toggle_option.dart b/packages/neon/neon/lib/src/settings/models/toggle_option.dart index e3ef5397..9b3ae94b 100644 --- a/packages/neon/neon/lib/src/settings/models/toggle_option.dart +++ b/packages/neon/neon/lib/src/settings/models/toggle_option.dart @@ -11,7 +11,7 @@ class ToggleOption extends Option { 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 { 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 { set value(final bool value) { super.value = value; - unawaited(storage.setBool(key, serialize())); + unawaited(storage.setBool(key.value, serialize())); } @override diff --git a/packages/neon/neon/lib/src/utils/account_options.dart b/packages/neon/neon/lib/src/utils/account_options.dart index 6125009d..61c2c5cc 100644 --- a/packages/neon/neon/lib/src/utils/account_options.dart +++ b/packages/neon/neon/lib/src/utils/account_options.dart @@ -47,9 +47,18 @@ class AccountSpecificOptions { late final initialApp = SelectOption( 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; +} diff --git a/packages/neon/neon/lib/src/utils/global_options.dart b/packages/neon/neon/lib/src/utils/global_options.dart index d6b319f1..e7081fef 100644 --- a/packages/neon/neon/lib/src/utils/global_options.dart +++ b/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 = { @@ -125,7 +125,7 @@ class GlobalOptions { late final themeMode = SelectOption( 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.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( 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( 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, diff --git a/packages/neon/neon/lib/src/utils/push_utils.dart b/packages/neon/neon/lib/src/utils/push_utils.dart index 1588dbce..35b7cd1a 100644 --- a/packages/neon/neon/lib/src/utils/push_utils.dart +++ b/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 loadRSAKeypair(final AppStorage storage) async { + static Future 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; @@ -93,12 +93,11 @@ class PushUtils { } else { final localizations = await appLocalizationsFromSystem(); - var accounts = []; + final accounts = loadAccounts(); Account? account; NotificationsNotification? notification; AndroidBitmap? largeIconBitmap; try { - accounts = loadAccounts(AppStorage('accounts')); account = accounts.tryFind(instance); if (account != null) { notification = diff --git a/packages/neon/neon/lib/src/utils/settings_export_helper.dart b/packages/neon/neon/lib/src/utils/settings_export_helper.dart index 3fe86dfa..d402eb5b 100644 --- a/packages/neon/neon/lib/src/utils/settings_export_helper.dart +++ b/packages/neon/neon/lib/src/utils/settings_export_helper.dart @@ -53,7 +53,7 @@ class SettingsExportHelper { Future _applyOptionsMapToOptions(final List