Browse Source

Merge pull request #480 from provokateurin/feature/option_listenable

Feature/option listenable
pull/487/head
Nikolas Rimikis 1 year ago committed by GitHub
parent
commit
e1bc0d5895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/neon/neon/lib/settings.dart
  2. 19
      packages/neon/neon/lib/src/app.dart
  3. 20
      packages/neon/neon/lib/src/blocs/accounts.dart
  4. 15
      packages/neon/neon/lib/src/blocs/apps.dart
  5. 8
      packages/neon/neon/lib/src/blocs/next_push.dart
  6. 66
      packages/neon/neon/lib/src/blocs/push_notifications.dart
  7. 2
      packages/neon/neon/lib/src/pages/account_settings.dart
  8. 7
      packages/neon/neon/lib/src/pages/home.dart
  9. 2
      packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart
  10. 17
      packages/neon/neon/lib/src/pages/settings.dart
  11. 4
      packages/neon/neon/lib/src/settings/models/nextcloud_app_options.dart
  12. 109
      packages/neon/neon/lib/src/settings/models/option.dart
  13. 68
      packages/neon/neon/lib/src/settings/models/select_option.dart
  14. 28
      packages/neon/neon/lib/src/settings/models/toggle_option.dart
  15. 26
      packages/neon/neon/lib/src/settings/widgets/checkbox_settings_tile.dart
  16. 85
      packages/neon/neon/lib/src/settings/widgets/dropdown_button_settings_tile.dart
  17. 24
      packages/neon/neon/lib/src/settings/widgets/option_builder.dart
  18. 11
      packages/neon/neon/lib/src/sort_box/sort_box_builder.dart
  19. 28
      packages/neon/neon/lib/src/utils/account_options.dart
  20. 114
      packages/neon/neon/lib/src/utils/global_options.dart
  21. 5
      packages/neon/neon/lib/src/utils/global_popups.dart
  22. 12
      packages/neon/neon/lib/src/utils/settings_export_helper.dart
  23. 1
      packages/neon/neon/pubspec.yaml
  24. 251
      packages/neon/neon/test/option_test.dart
  25. 19
      packages/neon/neon_files/lib/blocs/files.dart
  26. 18
      packages/neon/neon_files/lib/options.dart
  27. 27
      packages/neon/neon_files/lib/widgets/file_preview.dart
  28. 38
      packages/neon/neon_news/lib/options.dart
  29. 4
      packages/neon/neon_news/lib/widgets/folder_view.dart
  30. 20
      packages/neon/neon_notes/lib/options.dart

1
packages/neon/neon/lib/settings.dart

@ -3,4 +3,3 @@ export 'package:neon/src/settings/models/options_category.dart';
export 'package:neon/src/settings/models/select_option.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';
export 'package:neon/src/settings/models/toggle_option.dart'; export 'package:neon/src/settings/models/toggle_option.dart';
export 'package:neon/src/settings/widgets/option_builder.dart';

19
packages/neon/neon/lib/src/app.dart

@ -15,7 +15,6 @@ import 'package:neon/src/models/notifications_interface.dart';
import 'package:neon/src/models/push_notification.dart'; import 'package:neon/src/models/push_notification.dart';
import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/settings/widgets/option_builder.dart';
import 'package:neon/src/theme/neon.dart'; import 'package:neon/src/theme/neon.dart';
import 'package:neon/src/theme/theme.dart'; import 'package:neon/src/theme/theme.dart';
import 'package:neon/src/utils/global.dart'; import 'package:neon/src/utils/global.dart';
@ -102,8 +101,8 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
} }
if (_platform.canUseSystemTray) { if (_platform.canUseSystemTray) {
_globalOptions.systemTrayEnabled.stream.listen((final enabled) async { _globalOptions.systemTrayEnabled.addListener(() async {
if (enabled) { if (_globalOptions.systemTrayEnabled.value) {
// TODO: This works on Linux, but maybe not on macOS or Windows // TODO: This works on Linux, but maybe not on macOS or Windows
await tray.trayManager.setIcon('assets/logo.svg'); await tray.trayManager.setIcon('assets/logo.svg');
if (mounted) { if (mounted) {
@ -275,13 +274,13 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
} }
@override @override
Widget build(final BuildContext context) => OptionBuilder( Widget build(final BuildContext context) => ValueListenableBuilder(
option: _globalOptions.themeMode, valueListenable: _globalOptions.themeMode,
builder: (final context, final themeMode) => OptionBuilder( builder: (final context, final themeMode, final _) => ValueListenableBuilder(
option: _globalOptions.themeOLEDAsDark, valueListenable: _globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDark) => OptionBuilder( builder: (final context, final themeOLEDAsDark, final _) => ValueListenableBuilder(
option: _globalOptions.themeKeepOriginalAccentColor, valueListenable: _globalOptions.themeKeepOriginalAccentColor,
builder: (final context, final themeKeepOriginalAccentColor) => StreamBuilder<Account?>( builder: (final context, final themeKeepOriginalAccentColor, final _) => StreamBuilder<Account?>(
stream: _accountsBloc.activeAccount, stream: _accountsBloc.activeAccount,
builder: (final context, final activeAccountSnapshot) { builder: (final context, final activeAccountSnapshot) {
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();

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

@ -89,18 +89,14 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
} }
} }
unawaited( final account = as.tryFind(_globalOptions.initialAccount.value);
_globalOptions.initialAccount.stream.first.then((final lastAccount) { if (activeAccount.valueOrNull == null) {
final account = as.tryFind(lastAccount); if (account != null) {
if (activeAccount.valueOrNull == null) { setActiveAccount(account);
if (account != null) { } else if (as.isNotEmpty) {
setActiveAccount(account); setActiveAccount(as.first);
} else if (as.isNotEmpty) { }
setActiveAccount(as.first); }
}
}
}),
);
} }
final RequestManager _requestManager; final RequestManager _requestManager;

15
packages/neon/neon/lib/src/blocs/apps.dart

@ -49,21 +49,16 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
.add(result.transform((final data) => _filteredAppImplementations(data.map((final a) => a.id)))); .add(result.transform((final data) => _filteredAppImplementations(data.map((final a) => a.id))));
}); });
appImplementations.listen((final result) { appImplementations.listen((final result) async {
if (!result.hasData) { if (!result.hasData) {
return; return;
} }
final options = _accountsBloc.getOptionsFor(_account); final options = _accountsBloc.getOptionsFor(_account);
unawaited( final initialApp = options.initialApp.value ?? _getInitialAppFallback();
options.initialApp.stream.first.then((var initialApp) async { if (!activeApp.hasValue && initialApp != null) {
initialApp ??= _getInitialAppFallback(); await setActiveApp(initialApp);
}
if (!activeApp.hasValue && initialApp != null) {
await setActiveApp(initialApp);
}
}),
);
unawaited(_checkCompatibility()); unawaited(_checkCompatibility());
}); });

8
packages/neon/neon/lib/src/blocs/next_push.dart

@ -27,19 +27,15 @@ class NextPushBloc extends Bloc implements NextPushBlocEvents, NextPushBlocState
Rx.merge([ Rx.merge([
_globalOptions.pushNotificationsEnabled.stream, _globalOptions.pushNotificationsEnabled.stream,
_globalOptions.pushNotificationsDistributor.stream, _globalOptions.pushNotificationsDistributor.stream,
_globalOptions.pushNotificationsDistributor.values,
_accountsBloc.accounts, _accountsBloc.accounts,
]).debounceTime(const Duration(milliseconds: 100)).listen((final _) async { ]).debounceTime(const Duration(milliseconds: 100)).listen((final _) async {
if (!_globalOptions.pushNotificationsEnabled.enabled.hasValue || if (!_globalOptions.pushNotificationsEnabled.enabled || !_globalOptions.pushNotificationsEnabled.value) {
!_globalOptions.pushNotificationsEnabled.enabled.value ||
!_globalOptions.pushNotificationsEnabled.hasValue ||
!_globalOptions.pushNotificationsEnabled.value) {
return; return;
} }
if (_globalOptions.pushNotificationsDistributor.value != null) { if (_globalOptions.pushNotificationsDistributor.value != null) {
return; return;
} }
if (_globalOptions.pushNotificationsDistributor.values.value.containsKey(unifiedPushNextPushID)) { if (_globalOptions.pushNotificationsDistributor.values.containsKey(unifiedPushNextPushID)) {
// NextPush is already installed // NextPush is already installed
return; return;
} }

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

@ -32,16 +32,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
if (_platform.canUsePushNotifications) { if (_platform.canUsePushNotifications) {
unawaited(UnifiedPush.getDistributors().then(_globalOptions.updateDistributors)); unawaited(UnifiedPush.getDistributors().then(_globalOptions.updateDistributors));
_globalOptions.pushNotificationsEnabled.stream.listen((final enabled) async { _globalOptions.pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener);
if (enabled != _pushNotificationsEnabled) {
_pushNotificationsEnabled = enabled;
if (enabled) {
// We just use a single RSA keypair for all accounts
_keypair = await PushUtils.loadRSAKeypair(_storage);
await _setupUnifiedPush();
}
}
});
} }
} }
@ -50,14 +41,15 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
late final _storage = AppStorage('notifications', _sharedPreferences); late final _storage = AppStorage('notifications', _sharedPreferences);
final GlobalOptions _globalOptions; final GlobalOptions _globalOptions;
late RSAKeypair _keypair;
bool? _pushNotificationsEnabled;
final _notificationsController = StreamController<PushNotification>(); final _notificationsController = StreamController<PushNotification>();
StreamSubscription? _accountsListener;
@override @override
void dispose() { void dispose() {
unawaited(_notificationsController.close()); unawaited(_notificationsController.close());
unawaited(_accountsListener?.cancel());
_globalOptions.pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener);
} }
@override @override
@ -65,7 +57,22 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}'; String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}';
Future<void> _pushNotificationsEnabledListener() async {
if (_globalOptions.pushNotificationsEnabled.value) {
await _setupUnifiedPush();
_globalOptions.pushNotificationsDistributor.addListener(_distributorListener);
_accountsListener = _accountsBloc.accounts.listen(_registerUnifiedPushInstances);
} else {
_globalOptions.pushNotificationsDistributor.removeListener(_distributorListener);
unawaited(_accountsListener?.cancel());
}
}
Future _setupUnifiedPush() async { Future _setupUnifiedPush() async {
// We just use a single RSA keypair for all accounts
final keypair = await PushUtils.loadRSAKeypair(_storage);
await UnifiedPush.initialize( await UnifiedPush.initialize(
onNewEndpoint: (final endpoint, final instance) async { onNewEndpoint: (final endpoint, final instance) async {
final account = _accountsBloc.accounts.value.tryFind(instance); final account = _accountsBloc.accounts.value.tryFind(instance);
@ -83,7 +90,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
final subscription = await account.client.notifications.registerDevice( final subscription = await account.client.notifications.registerDevice(
pushTokenHash: generatePushTokenHash(endpoint), pushTokenHash: generatePushTokenHash(endpoint),
devicePublicKey: _keypair.publicKey.toFormattedPEM(), devicePublicKey: keypair.publicKey.toFormattedPEM(),
proxyServer: '$endpoint#', // This is a hack to make the Nextcloud server directly push to the endpoint proxyServer: '$endpoint#', // This is a hack to make the Nextcloud server directly push to the endpoint
); );
@ -95,24 +102,23 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
}, },
onMessage: PushUtils.onMessage, onMessage: PushUtils.onMessage,
); );
}
_globalOptions.pushNotificationsDistributor.stream.listen((final distributor) async { Future<void> _distributorListener() async {
final disabled = distributor == null; final distributor = _globalOptions.pushNotificationsDistributor.value;
final sameDistributor = distributor == await UnifiedPush.getDistributor(); final disabled = distributor == null;
final accounts = _accountsBloc.accounts.value; final sameDistributor = distributor == await UnifiedPush.getDistributor();
if (disabled || !sameDistributor) { final accounts = _accountsBloc.accounts.value;
await _unregisterUnifiedPushInstances(accounts); if (disabled || !sameDistributor) {
} await _unregisterUnifiedPushInstances(accounts);
if (!disabled && !sameDistributor) { }
debugPrint('UnifiedPush distributor changed to $distributor'); if (!disabled && !sameDistributor) {
await UnifiedPush.saveDistributor(distributor); debugPrint('UnifiedPush distributor changed to $distributor');
} await UnifiedPush.saveDistributor(distributor);
if (!disabled) { }
await _registerUnifiedPushInstances(accounts); if (!disabled) {
} await _registerUnifiedPushInstances(accounts);
}); }
_accountsBloc.accounts.listen(_registerUnifiedPushInstances);
} }
Future _unregisterUnifiedPushInstances(final List<Account> accounts) async { Future _unregisterUnifiedPushInstances(final List<Account> accounts) async {

2
packages/neon/neon/lib/src/pages/account_settings.dart

@ -66,7 +66,7 @@ class AccountSettingsPage extends StatelessWidget {
context, context,
AppLocalizations.of(context).settingsResetForConfirmation(name), AppLocalizations.of(context).settingsResetForConfirmation(name),
)) { )) {
await options.reset(); options.reset();
} }
}, },
tooltip: AppLocalizations.of(context).settingsResetFor(name), tooltip: AppLocalizations.of(context).settingsResetFor(name),

7
packages/neon/neon/lib/src/pages/home.dart

@ -7,7 +7,6 @@ import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/settings/widgets/option_builder.dart';
import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/global_options.dart';
import 'package:neon/src/utils/global_options.dart' as global_options; import 'package:neon/src/utils/global_options.dart' as global_options;
import 'package:neon/src/utils/global_popups.dart'; import 'package:neon/src/utils/global_popups.dart';
@ -150,9 +149,9 @@ class _HomePageState extends State<HomePage> {
}, },
); );
final body = OptionBuilder<global_options.NavigationMode>( final body = ValueListenableBuilder<global_options.NavigationMode>(
option: _globalOptions.navigationMode, valueListenable: _globalOptions.navigationMode,
builder: (final context, final navigationMode) { builder: (final context, final navigationMode, final _) {
final drawerAlwaysVisible = navigationMode == global_options.NavigationMode.drawerAlwaysVisible; final drawerAlwaysVisible = navigationMode == global_options.NavigationMode.drawerAlwaysVisible;
final body = Scaffold( final body = Scaffold(

2
packages/neon/neon/lib/src/pages/nextcloud_app_settings.dart

@ -30,7 +30,7 @@ class NextcloudAppSettingsPage extends StatelessWidget {
context, context,
AppLocalizations.of(context).settingsResetForConfirmation(appImplementation.name(context)), AppLocalizations.of(context).settingsResetForConfirmation(appImplementation.name(context)),
)) { )) {
await appImplementation.options.reset(); appImplementation.options.reset();
} }
}, },
tooltip: AppLocalizations.of(context).settingsResetFor(appImplementation.name(context)), tooltip: AppLocalizations.of(context).settingsResetFor(appImplementation.name(context)),

17
packages/neon/neon/lib/src/pages/settings.dart

@ -64,14 +64,14 @@ class _SettingsPageState extends State<SettingsPage> {
IconButton( IconButton(
onPressed: () async { onPressed: () async {
if (await showConfirmationDialog(context, AppLocalizations.of(context).settingsResetAllConfirmation)) { if (await showConfirmationDialog(context, AppLocalizations.of(context).settingsResetAllConfirmation)) {
await globalOptions.reset(); globalOptions.reset();
for (final appImplementation in appImplementations) { for (final appImplementation in appImplementations) {
await appImplementation.options.reset(); appImplementation.options.reset();
} }
for (final account in accountsBloc.accounts.value) { for (final account in accountsBloc.accounts.value) {
await accountsBloc.getOptionsFor(account).reset(); accountsBloc.getOptionsFor(account).reset();
} }
} }
}, },
@ -88,12 +88,12 @@ class _SettingsPageState extends State<SettingsPage> {
final accountsSnapshot, final accountsSnapshot,
) { ) {
final platform = Provider.of<NeonPlatform>(context, listen: false); final platform = Provider.of<NeonPlatform>(context, listen: false);
return StreamBuilder<bool>( return ValueListenableBuilder<bool>(
stream: globalOptions.pushNotificationsEnabled.enabled, valueListenable: globalOptions.pushNotificationsEnabled,
initialData: globalOptions.pushNotificationsEnabled.enabled.valueOrNull,
builder: ( builder: (
final context, final context,
final pushNotificationsEnabledEnabledSnapshot, final _,
final __,
) => ) =>
SettingsList( SettingsList(
initialCategory: widget.initialCategory?.name, initialCategory: widget.initialCategory?.name,
@ -144,8 +144,7 @@ class _SettingsPageState extends State<SettingsPage> {
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications), title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
key: ValueKey(SettingsCageories.pushNotifications.name), key: ValueKey(SettingsCageories.pushNotifications.name),
tiles: [ tiles: [
if (pushNotificationsEnabledEnabledSnapshot.hasData && if (!globalOptions.pushNotificationsEnabled.enabled) ...[
!pushNotificationsEnabledEnabledSnapshot.requireData) ...[
TextSettingsTile( TextSettingsTile(
text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice, text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice,
style: TextStyle( style: TextStyle(

4
packages/neon/neon/lib/src/settings/models/nextcloud_app_options.dart

@ -9,9 +9,9 @@ abstract class NextcloudAppOptions {
late final List<OptionsCategory> categories; late final List<OptionsCategory> categories;
late final List<Option> options; late final List<Option> options;
Future reset() async { void reset() {
for (final option in options) { for (final option in options) {
await option.reset(); option.reset();
} }
} }

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

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/options_category.dart'; import 'package:neon/src/settings/models/options_category.dart';
import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/settings/models/storage.dart';
@ -7,57 +8,107 @@ import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@internal @internal
class OptionDisableException implements Exception {} abstract class Option<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates an Option
@immutable
@internal
abstract class Option<T> {
Option({ Option({
required this.storage, required this.storage,
required this.key, required this.key,
required this.label, required this.label,
required this.defaultValue, required this.defaultValue,
required this.stream, final bool enabled = true,
this.category, this.category,
final BehaviorSubject<bool>? enabled, final T? initialValue,
}) : enabled = enabled ?? BehaviorSubject<bool>.seeded(true); }) : _value = initialValue ?? defaultValue,
_enabled = enabled;
/// Creates an Option depending on the State of another one.
Option.depend({
required this.storage,
required this.key,
required this.label,
required this.defaultValue,
required final ValueListenable<bool> enabled,
this.category,
final T? initialValue,
}) : _value = initialValue ?? defaultValue,
_enabled = enabled.value {
enabled.addListener(() {
this.enabled = enabled.value;
});
}
final SettingsStorage storage; final SettingsStorage storage;
final String key; final String key;
final LabelBuilder label; final LabelBuilder label;
final T defaultValue; final T defaultValue;
final OptionsCategory? category; final OptionsCategory? category;
final BehaviorSubject<bool> enabled;
final BehaviorSubject<T> stream;
T get value { T _value;
if (hasValue) {
return stream.value;
}
return defaultValue; /// The current value stored in this option.
///
/// When the value is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
@override
T get value => _value;
@mustCallSuper
set value(final T newValue) {
if (_value == newValue) {
return;
}
_value = newValue;
notifyListeners();
} }
bool get hasValue { bool _enabled;
if (!enabled.value) {
throw OptionDisableException();
}
return stream.hasValue; /// The current enabled state stored in this option.
///
/// When the value is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
bool get enabled => _enabled;
@mustCallSuper
set enabled(final bool newValue) {
if (_enabled == newValue) {
return;
}
_enabled = newValue;
notifyListeners();
} }
Future reset() async { /// Resets the option to its [default] value.
await set(defaultValue); void reset() {
value = defaultValue;
} }
void dispose() { /// Deserializes the data.
unawaited(stream.close()); T deserialize(final Object data);
unawaited(enabled.close());
} /// Serializes the [value].
Object serialize();
BehaviorSubject<T>? _stream;
Future set(final T value); /// A stream of values that is updated on each event.
///
/// This is similar to adding an [addListener] callback and getting the current data in it.
/// You should generally listen to the notifications directly.
Stream<T> get stream {
_stream ??= BehaviorSubject.seeded(_value);
Future<T?> deserialize(final dynamic data); addListener(() {
_stream!.add(_value);
});
dynamic serialize(); return _stream!;
}
@override
void dispose() {
unawaited(_stream?.close());
super.dispose();
}
} }

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

@ -1,51 +1,67 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:neon/src/settings/models/option.dart'; import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/widgets/label_builder.dart'; import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:rxdart/rxdart.dart';
class SelectOption<T> extends Option<T> { class SelectOption<T> extends Option<T> {
/// Creates a SelectOption
SelectOption({ SelectOption({
required super.storage, required super.storage,
required super.key, required super.key,
required super.label, required super.label,
required super.defaultValue, required super.defaultValue,
required this.values, required final Map<T, LabelBuilder> values,
super.category, super.category,
super.enabled, super.enabled,
}) : super(stream: BehaviorSubject()) { }) : _values = values,
unawaited( super(initialValue: _fromString(values, storage.getString(key)));
values.first.then((final vs) async {
final valueStr = storage.getString(key); /// Creates a SelectOption depending on the State of another [Option].
T? initialValue; SelectOption.depend({
required super.storage,
if (valueStr != null) { required super.key,
initialValue = _fromString(vs, valueStr); required super.label,
} required super.defaultValue,
stream.add(initialValue ?? defaultValue); required final Map<T, LabelBuilder> values,
}), required super.enabled,
); super.category,
} }) : _values = values,
super.depend(initialValue: _fromString(values, storage.getString(key)));
T? _fromString(final Map<T, LabelBuilder> vs, final String? valueStr) { static T? _fromString<T>(final Map<T, LabelBuilder> vs, final String? valueStr) {
final v = vs.keys.where((final e) => e.toString() == valueStr); if (valueStr == null) {
if (v.length == 1) { return null;
return v.first;
} }
return null;
return vs.keys.firstWhereOrNull((final e) => e.toString() == valueStr);
} }
final BehaviorSubject<Map<T, LabelBuilder>> values; Map<T, LabelBuilder> _values;
@override @override
Future set(final T value) { set value(final T value) {
stream.add(value); super.value = value;
return storage.setString(key, value.toString()); unawaited(storage.setString(key, serialize()));
}
/// A collection of different values this can have.
///
/// See:
/// * [value] for the currently selected one
Map<T, LabelBuilder> get values => _values;
set values(final Map<T, LabelBuilder> newValues) {
if (_values == newValues) {
return;
}
_values = newValues;
notifyListeners();
} }
@override @override
String? serialize() => value?.toString(); String serialize() => value.toString();
@override @override
Future<T?> deserialize(final dynamic data) async => _fromString(await values.first, data as String?); T deserialize(final Object data) => _fromString(_values, data as String)!;
} }

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

@ -1,25 +1,39 @@
import 'dart:async';
import 'package:neon/src/settings/models/option.dart'; import 'package:neon/src/settings/models/option.dart';
import 'package:rxdart/rxdart.dart';
class ToggleOption extends Option<bool> { class ToggleOption extends Option<bool> {
/// Creates a ToggleOption
ToggleOption({ ToggleOption({
required super.storage, required super.storage,
required super.key, required super.key,
required super.label, required super.label,
required super.defaultValue, required final bool defaultValue,
super.category, super.category,
super.enabled, super.enabled,
}) : super(stream: BehaviorSubject.seeded(storage.getBool(key) ?? defaultValue)); }) : super(defaultValue: storage.getBool(key) ?? defaultValue);
/// Creates a ToggleOption depending on the State of another [Option].
ToggleOption.depend({
required super.storage,
required super.key,
required super.label,
required final bool defaultValue,
required super.enabled,
super.category,
}) : super.depend(
defaultValue: storage.getBool(key) ?? defaultValue,
);
@override @override
Future set(final bool value) { set value(final bool value) {
stream.add(value); super.value = value;
return storage.setBool(key, value); unawaited(storage.setBool(key, serialize()));
} }
@override @override
bool serialize() => value; bool serialize() => value;
@override @override
Future<bool?> deserialize(final dynamic data) async => data as bool; bool deserialize(final Object data) => data as bool;
} }

26
packages/neon/neon/lib/src/settings/widgets/checkbox_settings_tile.dart

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/toggle_option.dart'; import 'package:neon/src/settings/models/toggle_option.dart';
import 'package:neon/src/settings/widgets/option_builder.dart';
import 'package:neon/src/settings/widgets/settings_tile.dart'; import 'package:neon/src/settings/widgets/settings_tile.dart';
@internal @internal
@ -12,21 +11,16 @@ class CheckBoxSettingsTile extends InputSettingsTile<ToggleOption> {
}); });
@override @override
Widget build(final BuildContext context) => OptionBuilder<bool>( Widget build(final BuildContext context) => ValueListenableBuilder(
option: option, valueListenable: option,
builder: (final context, final value) => StreamBuilder<bool>( builder: (final context, final value, final child) => CheckboxListTile.adaptive(
stream: option.enabled, enabled: option.enabled,
builder: (final context, final enabledSnapshot) => !enabledSnapshot.hasData title: child,
? const SizedBox() value: value,
: CheckboxListTile( onChanged: (final value) {
title: Text(option.label(context)), option.value = value!;
value: value, },
onChanged: enabledSnapshot.requireData
? (final value) async {
await option.set(value!);
}
: null,
),
), ),
child: Text(option.label(context)),
); );
} }

85
packages/neon/neon/lib/src/settings/widgets/dropdown_button_settings_tile.dart

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/select_option.dart'; import 'package:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:neon/src/settings/widgets/option_builder.dart';
import 'package:neon/src/settings/widgets/settings_tile.dart'; import 'package:neon/src/settings/widgets/settings_tile.dart';
@internal @internal
@ -13,64 +11,43 @@ class DropdownButtonSettingsTile<T> extends InputSettingsTile<SelectOption<T>> {
}); });
@override @override
Widget build(final BuildContext context) => OptionBuilder<T>( Widget build(final BuildContext context) => ValueListenableBuilder(
option: option, valueListenable: option,
builder: ( builder: (final context, final value, final child) => LayoutBuilder(
final context, builder: (final context, final constraints) => ListTile(
final value, enabled: option.enabled,
) => title: child,
StreamBuilder<bool>( trailing: ConstrainedBox(
stream: option.enabled, constraints: BoxConstraints(
builder: ( maxWidth: constraints.maxWidth * 0.5,
final context, ),
final enabledSnapshot, child: IntrinsicWidth(
) => child: DropdownButton<T>(
StreamBuilder<Map<T, LabelBuilder>>( isExpanded: true,
stream: option.values, value: value,
builder: ( items: option.values.keys
final context, .map(
final valuesSnapshot, (final k) => DropdownMenuItem(
) => value: k,
LayoutBuilder( child: Text(
builder: (final context, final constraints) => ListTile( option.values[k]!(context),
title: Text( overflow: TextOverflow.ellipsis,
option.label(context),
style: enabledSnapshot.data ?? false
? null
: Theme.of(context).textTheme.titleMedium!.copyWith(color: Theme.of(context).disabledColor),
),
trailing: valuesSnapshot.hasData
? ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.5,
),
child: IntrinsicWidth(
child: DropdownButton<T>(
isExpanded: true,
value: value,
items: valuesSnapshot.requireData.keys
.map(
(final k) => DropdownMenuItem(
value: k,
child: Text(
valuesSnapshot.requireData[k]!(context),
overflow: TextOverflow.ellipsis,
),
),
)
.toList(),
onChanged: enabledSnapshot.data ?? false
? (final value) async {
await option.set(value as T);
}
: null,
), ),
), ),
) )
: null, .toList(),
onChanged: option.enabled
? (final value) {
option.value = value as T;
}
: null,
),
), ),
), ),
), ),
), ),
child: Text(
option.label(context),
),
); );
} }

24
packages/neon/neon/lib/src/settings/widgets/option_builder.dart

@ -1,24 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:neon/src/settings/models/option.dart';
typedef OptionBuilderFunction<T> = Widget Function(BuildContext context, T snapshot);
class OptionBuilder<T> extends StreamBuilderBase<T, T> {
OptionBuilder({
required this.option,
required this.builder,
super.key,
}) : super(stream: option.stream);
final Option<T> option;
final OptionBuilderFunction<T> builder;
@override
T afterData(final T current, final T data) => data;
@override
T initial() => option.defaultValue;
@override
Widget build(final BuildContext context, final T currentSummary) => builder(context, currentSummary);
}

11
packages/neon/neon/lib/src/sort_box/sort_box_builder.dart

@ -1,6 +1,5 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:neon/src/settings/models/select_option.dart'; import 'package:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/widgets/option_builder.dart';
import 'package:sort_box/sort_box.dart'; import 'package:sort_box/sort_box.dart';
class SortBoxBuilder<T extends Enum, R> extends StatelessWidget { class SortBoxBuilder<T extends Enum, R> extends StatelessWidget {
@ -25,11 +24,11 @@ class SortBoxBuilder<T extends Enum, R> extends StatelessWidget {
return builder(context, null); return builder(context, null);
} }
return OptionBuilder<T>( return ValueListenableBuilder<T>(
option: sortPropertyOption, valueListenable: sortPropertyOption,
builder: (final context, final property) => OptionBuilder<SortBoxOrder>( builder: (final context, final property, final _) => ValueListenableBuilder<SortBoxOrder>(
option: sortBoxOrderOption, valueListenable: sortBoxOrderOption,
builder: (final context, final order) { builder: (final context, final order, final _) {
final box = Box(property, order); final box = Box(property, order);
return builder(context, sortBox.sort(input!, box)); return builder(context, sortBox.sort(input!, box));

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

@ -1,13 +1,9 @@
import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/settings/models/option.dart'; import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/select_option.dart'; import 'package:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:rxdart/rxdart.dart';
@internal @internal
class AccountSpecificOptions { class AccountSpecificOptions {
@ -16,33 +12,33 @@ class AccountSpecificOptions {
this._appsBloc, this._appsBloc,
) { ) {
_appsBloc.appImplementations.listen((final result) { _appsBloc.appImplementations.listen((final result) {
if (result.hasData) { if (!result.hasData) {
_appIDsSubject.add({ return;
null: (final context) => AppLocalizations.of(context).accountOptionsAutomatic,
for (final app in result.requireData) ...{
app.id: app.name,
},
});
} }
initialApp.values = {
null: (final context) => AppLocalizations.of(context).accountOptionsAutomatic,
for (final app in result.requireData) ...{
app.id: app.name,
},
};
}); });
} }
final AppStorage _storage; final AppStorage _storage;
final AppsBloc _appsBloc; final AppsBloc _appsBloc;
final _appIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
late final List<Option> options = [ late final List<Option> options = [
initialApp, initialApp,
]; ];
Future reset() async { void reset() {
for (final option in options) { for (final option in options) {
await option.reset(); option.reset();
} }
} }
void dispose() { void dispose() {
unawaited(_appIDsSubject.close());
for (final option in options) { for (final option in options) {
option.dispose(); option.dispose();
} }
@ -53,6 +49,6 @@ class AccountSpecificOptions {
key: 'initial-app', key: 'initial-app',
label: (final context) => AppLocalizations.of(context).accountOptionsInitialApp, label: (final context) => AppLocalizations.of(context).accountOptionsInitialApp,
defaultValue: null, defaultValue: null,
values: _appIDsSubject, values: {},
); );
} }

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

@ -9,10 +9,8 @@ import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/select_option.dart'; import 'package:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/settings/models/toggle_option.dart'; import 'package:neon/src/settings/models/toggle_option.dart';
import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
const unifiedPushNextPushID = 'org.unifiedpush.distributor.nextpush'; const unifiedPushNextPushID = 'org.unifiedpush.distributor.nextpush';
@ -23,54 +21,38 @@ class GlobalOptions {
this._sharedPreferences, this._sharedPreferences,
this._packageInfo, this._packageInfo,
) { ) {
themeMode.stream.listen((final value) { pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener);
_themeOLEDAsDarkEnabledSubject.add(value != ThemeMode.light); rememberLastUsedAccount.addListener(_rememberLastUsedAccountListener);
}); }
_pushNotificationsDistributorsSubject.listen((final distributors) async { void _rememberLastUsedAccountListener() {
final allowed = distributors.isNotEmpty; initialAccount.enabled = !rememberLastUsedAccount.value;
_pushNotificationsEnabledEnabledSubject.add(allowed); if (rememberLastUsedAccount.value) {
if (!allowed) { initialAccount.value = null;
await pushNotificationsEnabled.set(false); } else {
} // Only override the initial account if there already has been a value,
}); // which means it's not the initial emit from rememberLastUsedAccount
initialAccount.value = initialAccount.values.keys.first;
pushNotificationsEnabled.stream.listen((final enabled) async { }
if (enabled) { }
final response = await Permission.notification.request();
if (response.isPermanentlyDenied) { Future<void> _pushNotificationsEnabledListener() async {
_pushNotificationsEnabledEnabledSubject.add(false); if (pushNotificationsEnabled.value) {
} final response = await Permission.notification.request();
if (!response.isGranted) { if (response.isPermanentlyDenied) {
await pushNotificationsEnabled.set(false); pushNotificationsEnabled.enabled = false;
}
} else {
await pushNotificationsDistributor.set(null);
} }
}); if (!response.isGranted) {
pushNotificationsEnabled.value = false;
rememberLastUsedAccount.stream.listen((final remember) async {
_initialAccountEnabledSubject.add(!remember);
if (remember) {
await initialAccount.set(null);
} else {
// Only override the initial account if there already has been a value,
// which means it's not the initial emit from rememberLastUsedAccount
if (initialAccount.hasValue) {
await initialAccount.set((await initialAccount.values.first).keys.first);
}
} }
}); } else {
pushNotificationsDistributor.value = null;
}
} }
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
late final AppStorage _storage = AppStorage('global', _sharedPreferences); late final AppStorage _storage = AppStorage('global', _sharedPreferences);
final PackageInfo _packageInfo; final PackageInfo _packageInfo;
final _themeOLEDAsDarkEnabledSubject = BehaviorSubject<bool>();
final _pushNotificationsEnabledEnabledSubject = BehaviorSubject<bool>();
final _pushNotificationsDistributorsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
final _accountsIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
final _initialAccountEnabledSubject = BehaviorSubject<bool>();
late final _distributorsMap = <String, String Function(BuildContext)>{ late final _distributorsMap = <String, String Function(BuildContext)>{
_packageInfo.packageName: (final context) => _packageInfo.packageName: (final context) =>
@ -103,37 +85,44 @@ class GlobalOptions {
navigationMode, navigationMode,
]; ];
Future reset() async { void reset() {
for (final option in options) { for (final option in options) {
await option.reset(); option.reset();
} }
} }
void dispose() { void dispose() {
unawaited(_accountsIDsSubject.close());
unawaited(_themeOLEDAsDarkEnabledSubject.close());
for (final option in options) { for (final option in options) {
option.dispose(); option.dispose();
} }
pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener);
rememberLastUsedAccount.removeListener(_rememberLastUsedAccountListener);
} }
void updateAccounts(final List<Account> accounts) { void updateAccounts(final List<Account> accounts) {
if (accounts.isEmpty) { if (accounts.isEmpty) {
return; return;
} }
_accountsIDsSubject.add({ initialAccount.values = {
for (final account in accounts) ...{ for (final account in accounts) ...{
account.id: (final context) => account.client.humanReadableID, account.id: (final context) => account.client.humanReadableID,
}, },
}); };
} }
Future updateDistributors(final List<String> distributors) async { Future updateDistributors(final List<String> distributors) async {
_pushNotificationsDistributorsSubject.add({ pushNotificationsDistributor.values = {
for (final distributor in distributors) ...{ for (final distributor in distributors) ...{
distributor: _distributorsMap[distributor] ?? (final _) => distributor, distributor: _distributorsMap[distributor] ?? (final _) => distributor,
}, },
}); };
final allowed = distributors.isNotEmpty;
pushNotificationsEnabled.enabled = allowed;
if (!allowed) {
pushNotificationsEnabled.value = false;
}
} }
late final themeMode = SelectOption<ThemeMode>( late final themeMode = SelectOption<ThemeMode>(
@ -141,11 +130,11 @@ class GlobalOptions {
key: 'theme-mode', key: 'theme-mode',
label: (final context) => AppLocalizations.of(context).globalOptionsThemeMode, label: (final context) => AppLocalizations.of(context).globalOptionsThemeMode,
defaultValue: ThemeMode.system, defaultValue: ThemeMode.system,
values: BehaviorSubject.seeded({ values: {
ThemeMode.light: (final context) => AppLocalizations.of(context).globalOptionsThemeModeLight, ThemeMode.light: (final context) => AppLocalizations.of(context).globalOptionsThemeModeLight,
ThemeMode.dark: (final context) => AppLocalizations.of(context).globalOptionsThemeModeDark, ThemeMode.dark: (final context) => AppLocalizations.of(context).globalOptionsThemeModeDark,
ThemeMode.system: (final context) => AppLocalizations.of(context).globalOptionsThemeModeAutomatic, ThemeMode.system: (final context) => AppLocalizations.of(context).globalOptionsThemeModeAutomatic,
}), },
); );
late final themeOLEDAsDark = ToggleOption( late final themeOLEDAsDark = ToggleOption(
@ -153,7 +142,6 @@ class GlobalOptions {
key: 'theme-oled-as-dark', key: 'theme-oled-as-dark',
label: (final context) => AppLocalizations.of(context).globalOptionsThemeOLEDAsDark, label: (final context) => AppLocalizations.of(context).globalOptionsThemeOLEDAsDark,
defaultValue: false, defaultValue: false,
enabled: _themeOLEDAsDarkEnabledSubject,
); );
late final themeKeepOriginalAccentColor = ToggleOption( late final themeKeepOriginalAccentColor = ToggleOption(
@ -168,16 +156,15 @@ class GlobalOptions {
key: 'push-notifications-enabled', key: 'push-notifications-enabled',
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled, label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled,
defaultValue: false, defaultValue: false,
enabled: _pushNotificationsEnabledEnabledSubject,
); );
late final pushNotificationsDistributor = SelectOption<String?>( late final pushNotificationsDistributor = SelectOption<String?>.depend(
storage: _storage, storage: _storage,
key: 'push-notifications-distributor', key: 'push-notifications-distributor',
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsDistributor, label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsDistributor,
defaultValue: null, defaultValue: null,
values: _pushNotificationsDistributorsSubject, values: {},
enabled: pushNotificationsEnabled.stream, enabled: pushNotificationsEnabled,
); );
late final startupMinimized = ToggleOption( late final startupMinimized = ToggleOption(
@ -203,12 +190,12 @@ class GlobalOptions {
defaultValue: false, defaultValue: false,
); );
late final systemTrayHideToTrayWhenMinimized = ToggleOption( late final systemTrayHideToTrayWhenMinimized = ToggleOption.depend(
storage: _storage, storage: _storage,
key: 'systemtray-hide-to-tray-when-minimized', key: 'systemtray-hide-to-tray-when-minimized',
label: (final context) => AppLocalizations.of(context).globalOptionsSystemTrayHideToTrayWhenMinimized, label: (final context) => AppLocalizations.of(context).globalOptionsSystemTrayHideToTrayWhenMinimized,
defaultValue: true, defaultValue: true,
enabled: systemTrayEnabled.stream, enabled: systemTrayEnabled,
); );
late final rememberLastUsedAccount = ToggleOption( late final rememberLastUsedAccount = ToggleOption(
@ -223,8 +210,7 @@ class GlobalOptions {
key: 'initial-account', key: 'initial-account',
label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount, label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount,
defaultValue: null, defaultValue: null,
values: _accountsIDsSubject, values: {},
enabled: _initialAccountEnabledSubject,
); );
late final navigationMode = SelectOption<NavigationMode>( late final navigationMode = SelectOption<NavigationMode>(
@ -232,7 +218,7 @@ class GlobalOptions {
key: 'navigation-mode', key: 'navigation-mode',
label: (final context) => AppLocalizations.of(context).globalOptionsNavigationMode, label: (final context) => AppLocalizations.of(context).globalOptionsNavigationMode,
defaultValue: Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible, defaultValue: Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible,
values: BehaviorSubject.seeded({ values: {
NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer, NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer,
if (!Platform.isAndroid && !Platform.isIOS) ...{ if (!Platform.isAndroid && !Platform.isIOS) ...{
NavigationMode.drawerAlwaysVisible: (final context) => NavigationMode.drawerAlwaysVisible: (final context) =>
@ -240,7 +226,7 @@ class GlobalOptions {
}, },
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar, NavigationMode.quickBar: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeQuickBar,
}), },
); );
} }

5
packages/neon/neon/lib/src/utils/global_popups.dart

@ -32,9 +32,8 @@ class GlobalPopups {
final firstLaunchBloc = Provider.of<FirstLaunchBloc>(context, listen: false); final firstLaunchBloc = Provider.of<FirstLaunchBloc>(context, listen: false);
final nextPushBloc = Provider.of<NextPushBloc>(context, listen: false); final nextPushBloc = Provider.of<NextPushBloc>(context, listen: false);
firstLaunchBloc.onFirstLaunch.listen((final _) async { firstLaunchBloc.onFirstLaunch.listen((final _) {
if (await globalOptions.pushNotificationsEnabled.enabled.first) { if (globalOptions.pushNotificationsEnabled.enabled) {
// ignore: use_build_context_synchronously
if (!context.mounted) { if (!context.mounted) {
return; return;
} }

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

@ -54,7 +54,11 @@ class SettingsExportHelper {
for (final optionKey in data.keys) { for (final optionKey in data.keys) {
for (final option in options) { for (final option in options) {
if (option.key == optionKey) { if (option.key == optionKey) {
await option.set(await option.deserialize(data[optionKey])); final Object? value = data[optionKey];
if (value != null) {
option.value = await option.deserialize(value);
}
} }
} }
} }
@ -63,7 +67,7 @@ class SettingsExportHelper {
Map<String, dynamic> toJsonExport() => { Map<String, dynamic> toJsonExport() => {
'global': { 'global': {
for (final option in globalOptions.options) ...{ for (final option in globalOptions.options) ...{
if (option.enabled.value) ...{ if (option.enabled) ...{
option.key: option.serialize(), option.key: option.serialize(),
}, },
}, },
@ -72,7 +76,7 @@ class SettingsExportHelper {
for (final appImplementation in appImplementations) ...{ for (final appImplementation in appImplementations) ...{
appImplementation.id: { appImplementation.id: {
for (final option in appImplementation.options.options) ...{ for (final option in appImplementation.options.options) ...{
if (option.enabled.value) ...{ if (option.enabled) ...{
option.key: option.serialize(), option.key: option.serialize(),
}, },
}, },
@ -83,7 +87,7 @@ class SettingsExportHelper {
for (final account in accountSpecificOptions.keys) ...{ for (final account in accountSpecificOptions.keys) ...{
account.id: { account.id: {
for (final option in accountSpecificOptions[account]!) ...{ for (final option in accountSpecificOptions[account]!) ...{
if (option.enabled.value) ...{ if (option.enabled) ...{
option.key: option.serialize(), option.key: option.serialize(),
}, },
}, },

1
packages/neon/neon/pubspec.yaml

@ -60,6 +60,7 @@ dev_dependencies:
build_runner: ^2.4.4 build_runner: ^2.4.4
go_router_builder: ^2.2.1 go_router_builder: ^2.2.1
json_serializable: ^6.6.2 json_serializable: ^6.6.2
mocktail: ^0.3.0
nit_picking: nit_picking:
git: git:
url: https://github.com/stack11/dart_nit_picking url: https://github.com/stack11/dart_nit_picking

251
packages/neon/neon/test/option_test.dart

@ -0,0 +1,251 @@
// ignore_for_file: discarded_futures
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:mocktail/mocktail.dart';
import 'package:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/settings/models/toggle_option.dart';
import 'package:test/test.dart';
class MockStorage extends Mock implements SettingsStorage {}
class MockCallbackFunction extends Mock {
FutureOr<void> call();
}
enum SelectValues {
first,
second,
third,
}
void main() {
final storage = MockStorage();
const key = 'storage-key';
String labelBuilder(final _) => 'label';
group('SelectOption', () {
final valuesLabel = {
SelectValues.first: (final _) => 'first',
SelectValues.second: (final _) => 'second',
SelectValues.third: (final _) => 'third',
};
late SelectOption<SelectValues> option;
setUp(() {
when(() => storage.setString(key, any())).thenAnswer((final _) async {});
option = SelectOption<SelectValues>(
storage: storage,
key: key,
label: labelBuilder,
defaultValue: SelectValues.first,
values: valuesLabel,
);
});
tearDown(() {
reset(storage);
option.dispose();
});
test('Create', () {
expect(option.value, option.defaultValue, reason: 'Should default to defaultValue.');
when(() => storage.getString(key)).thenReturn('SelectValues.second');
option = SelectOption<SelectValues>(
storage: storage,
key: key,
label: labelBuilder,
defaultValue: SelectValues.first,
values: valuesLabel,
);
expect(option.value, SelectValues.second, reason: 'Should load value from storage when available.');
});
test('Depend', () {
final enabled = ValueNotifier(false);
final callback = MockCallbackFunction();
option = SelectOption<SelectValues>.depend(
storage: storage,
key: key,
label: labelBuilder,
defaultValue: SelectValues.first,
values: valuesLabel,
enabled: enabled,
)..addListener(callback.call);
expect(option.enabled, enabled.value, reason: 'Should initialize with enabled value.');
enabled.value = true;
verify(callback.call).called(1);
expect(option.enabled, enabled.value, reason: 'Should update the enabled state.');
});
test('Update', () {
final callback = MockCallbackFunction();
option
..addListener(callback.call)
..value = SelectValues.third;
verify(callback.call).called(1);
verify(() => storage.setString(key, 'SelectValues.third')).called(1);
expect(option.value, SelectValues.third, reason: 'Should update the value.');
option.value = SelectValues.third;
verifyNever(callback.call); // Don't notify with the same value
expect(option.value, SelectValues.third, reason: 'Should keep the value.');
});
test('Disable', () {
final callback = MockCallbackFunction();
option.addListener(callback.call);
expect(option.enabled, true, reason: 'Should default to enabled');
option.enabled = false;
verify(callback.call).called(1);
expect(option.enabled, false, reason: 'Should disable option.');
option.enabled = false;
verifyNever(callback.call); // Don't notify with the same value
expect(option.enabled, false, reason: 'Should keep the value.');
});
test('Change values', () {
final callback = MockCallbackFunction();
option.addListener(callback.call);
expect(option.values, equals(valuesLabel));
final newValues = {
SelectValues.second: (final _) => 'second',
SelectValues.third: (final _) => 'third',
};
option.values = newValues;
verify(callback.call).called(1);
expect(option.values, equals(newValues), reason: 'Should change values.');
option.values = newValues;
verifyNever(callback.call); // Don't notify with the same value
expect(option.values, newValues, reason: 'Should keep the values.');
});
test('Reset', () {
final callback = MockCallbackFunction();
option
..value = SelectValues.third
..addListener(callback.call);
expect(option.value, SelectValues.third);
option.reset();
verify(callback.call).called(1);
expect(option.value, option.defaultValue, reason: 'Should reset the value.');
});
});
group('ToggleOption', () {
late ToggleOption option;
setUp(() {
when(() => storage.setBool(key, any())).thenAnswer((final _) async {});
option = ToggleOption(
storage: storage,
key: key,
label: labelBuilder,
defaultValue: true,
);
});
tearDown(() {
reset(storage);
option.dispose();
});
test('Create', () {
expect(option.value, option.defaultValue, reason: 'Should default to defaultValue.');
when(() => storage.getBool(key)).thenReturn(true);
option = ToggleOption(
storage: storage,
key: key,
label: labelBuilder,
defaultValue: false,
);
expect(option.value, true, reason: 'Should load value from storage when available.');
});
test('Depend', () {
final enabled = ValueNotifier(false);
final callback = MockCallbackFunction();
option = ToggleOption.depend(
storage: storage,
key: key,
label: labelBuilder,
defaultValue: true,
enabled: enabled,
)..addListener(callback.call);
expect(option.enabled, enabled.value, reason: 'Should initialize with enabled value.');
enabled.value = true;
verify(callback.call).called(1);
expect(option.enabled, enabled.value, reason: 'Should update the enabled state.');
});
test('Update', () {
final callback = MockCallbackFunction();
option
..addListener(callback.call)
..value = false;
verify(callback.call).called(1);
verify(() => storage.setBool(key, false)).called(1);
expect(option.value, false, reason: 'Should update the value.');
option.value = false;
verifyNever(callback.call); // Don't notify with the same value
expect(option.value, false, reason: 'Should keep the value.');
});
test('Disable', () {
final callback = MockCallbackFunction();
option.addListener(callback.call);
expect(option.enabled, true, reason: 'Should default to enabled');
option.enabled = false;
verify(callback.call).called(1);
expect(option.enabled, false, reason: 'Should disable option.');
option.enabled = false;
verifyNever(callback.call); // Don't notify with the same value
expect(option.enabled, false, reason: 'Should keep the value.');
});
test('Reset', () {
final callback = MockCallbackFunction();
option
..value = false
..addListener(callback.call);
expect(option.value, false);
option.reset();
verify(callback.call).called(1);
expect(option.value, option.defaultValue, reason: 'Should reset the value.');
});
});
}

19
packages/neon/neon_files/lib/blocs/files.dart

@ -33,12 +33,8 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
this._requestManager, this._requestManager,
this._platform, this._platform,
) { ) {
options.uploadQueueParallelism.stream.listen((final value) { options.uploadQueueParallelism.addListener(_uploadParalelismListener);
_uploadQueue.parallel = value; options.downloadQueueParallelism.addListener(_downloadParalelismListener);
});
options.downloadQueueParallelism.stream.listen((final value) {
_downloadQueue.parallel = value;
});
} }
final FilesAppSpecificOptions options; final FilesAppSpecificOptions options;
@ -56,6 +52,9 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
_downloadQueue.dispose(); _downloadQueue.dispose();
unawaited(uploadTasks.close()); unawaited(uploadTasks.close());
unawaited(downloadTasks.close()); unawaited(downloadTasks.close());
options.uploadQueueParallelism.removeListener(_uploadParalelismListener);
options.downloadQueueParallelism.removeListener(_downloadParalelismListener);
} }
@override @override
@ -196,4 +195,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
} }
FilesBrowserBloc getNewFilesBrowserBloc() => FilesBrowserBloc(_requestManager, options, client); FilesBrowserBloc getNewFilesBrowserBloc() => FilesBrowserBloc(_requestManager, options, client);
void _downloadParalelismListener() {
_downloadQueue.parallel = options.downloadQueueParallelism.value;
}
void _uploadParalelismListener() {
_uploadQueue.parallel = options.uploadQueueParallelism.value;
}
} }

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

@ -26,12 +26,12 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'files-sort-property', key: 'files-sort-property',
label: (final context) => AppLocalizations.of(context).optionsFilesSortProperty, label: (final context) => AppLocalizations.of(context).optionsFilesSortProperty,
defaultValue: FilesSortProperty.name, defaultValue: FilesSortProperty.name,
values: BehaviorSubject.seeded({ values: {
FilesSortProperty.name: (final context) => AppLocalizations.of(context).optionsFilesSortPropertyName, FilesSortProperty.name: (final context) => AppLocalizations.of(context).optionsFilesSortPropertyName,
FilesSortProperty.modifiedDate: (final context) => FilesSortProperty.modifiedDate: (final context) =>
AppLocalizations.of(context).optionsFilesSortPropertyModifiedDate, AppLocalizations.of(context).optionsFilesSortPropertyModifiedDate,
FilesSortProperty.size: (final context) => AppLocalizations.of(context).optionsFilesSortPropertySize, FilesSortProperty.size: (final context) => AppLocalizations.of(context).optionsFilesSortPropertySize,
}), },
); );
late final filesSortBoxOrderOption = SelectOption<SortBoxOrder>( late final filesSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -40,7 +40,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'files-sort-box-order', key: 'files-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsFilesSortOrder, label: (final context) => AppLocalizations.of(context).optionsFilesSortOrder,
defaultValue: SortBoxOrder.ascending, defaultValue: SortBoxOrder.ascending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues), values: sortBoxOrderOptionValues,
); );
late final showPreviewsOption = ToggleOption( late final showPreviewsOption = ToggleOption(
@ -57,11 +57,11 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'upload-queue-parallelism', key: 'upload-queue-parallelism',
label: (final context) => AppLocalizations.of(context).optionsUploadQueueParallelism, label: (final context) => AppLocalizations.of(context).optionsUploadQueueParallelism,
defaultValue: 4, defaultValue: 4,
values: BehaviorSubject.seeded({ values: {
for (var i = 1; i <= 16; i = i * 2) ...{ for (var i = 1; i <= 16; i = i * 2) ...{
i: (final _) => i.toString(), i: (final _) => i.toString(),
}, },
}), },
); );
late final downloadQueueParallelism = SelectOption<int>( late final downloadQueueParallelism = SelectOption<int>(
@ -70,11 +70,11 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'download-queue-parallelism', key: 'download-queue-parallelism',
label: (final context) => AppLocalizations.of(context).optionsDownloadQueueParallelism, label: (final context) => AppLocalizations.of(context).optionsDownloadQueueParallelism,
defaultValue: 4, defaultValue: 4,
values: BehaviorSubject.seeded({ values: {
for (var i = 1; i <= 16; i = i * 2) ...{ for (var i = 1; i <= 16; i = i * 2) ...{
i: (final _) => i.toString(), i: (final _) => i.toString(),
}, },
}), },
); );
late final _sizeWarningValues = <int?, String Function(BuildContext)>{ late final _sizeWarningValues = <int?, String Function(BuildContext)>{
@ -100,7 +100,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'upload-size-warning', key: 'upload-size-warning',
label: (final context) => AppLocalizations.of(context).optionsUploadSizeWarning, label: (final context) => AppLocalizations.of(context).optionsUploadSizeWarning,
defaultValue: _mb(10), defaultValue: _mb(10),
values: BehaviorSubject.seeded(_sizeWarningValues), values: _sizeWarningValues,
); );
late final downloadSizeWarning = SelectOption<int?>( late final downloadSizeWarning = SelectOption<int?>(
@ -109,7 +109,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'download-size-warning', key: 'download-size-warning',
label: (final context) => AppLocalizations.of(context).optionsDownloadSizeWarning, label: (final context) => AppLocalizations.of(context).optionsDownloadSizeWarning,
defaultValue: _mb(10), defaultValue: _mb(10),
values: BehaviorSubject.seeded(_sizeWarningValues), values: _sizeWarningValues,
); );
} }

27
packages/neon/neon_files/lib/widgets/file_preview.dart

@ -37,31 +37,34 @@ class FilePreview extends StatelessWidget {
); );
} }
return OptionBuilder<bool>( return ValueListenableBuilder<bool>(
option: bloc.options.showPreviewsOption, valueListenable: bloc.options.showPreviewsOption,
builder: (final context, final showPreviewsSnapshot) { builder: (final context, final showPreviews, final child) {
if (showPreviewsSnapshot && (details.hasPreview ?? false)) { if (showPreviews && (details.hasPreview ?? false)) {
final account = Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!; final account = Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!;
final child = FilePreviewImage( final preview = FilePreviewImage(
account: account, account: account,
file: details, file: details,
size: size, size: size,
); );
if (withBackground) { if (withBackground) {
return NeonImageWrapper( return NeonImageWrapper(
borderRadius: borderRadius, borderRadius: borderRadius,
child: child, child: preview,
); );
} }
return child;
return preview;
} }
return FileIcon( return child!;
details.name,
color: color,
size: size.shortestSide,
);
}, },
child: FileIcon(
details.name,
color: color,
size: size.shortestSide,
),
); );
}, },
), ),

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

@ -22,7 +22,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
feedsSortBoxOrderOption, feedsSortBoxOrderOption,
]; ];
_articleViewTypeValuesSubject.add({ articleViewTypeOption.values = {
ArticleViewType.direct: (final context) => AppLocalizations.of(context).optionsArticleViewTypeDirect, ArticleViewType.direct: (final context) => AppLocalizations.of(context).optionsArticleViewTypeDirect,
if (platform.canUseWebView) ...{ if (platform.canUseWebView) ...{
ArticleViewType.internalBrowser: (final context) => ArticleViewType.internalBrowser: (final context) =>
@ -30,11 +30,9 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
}, },
ArticleViewType.externalBrowser: (final context) => ArticleViewType.externalBrowser: (final context) =>
AppLocalizations.of(context).optionsArticleViewTypeExternalBrowser, AppLocalizations.of(context).optionsArticleViewTypeExternalBrowser,
}); };
} }
final _articleViewTypeValuesSubject = BehaviorSubject<Map<ArticleViewType, String Function(BuildContext)>>();
final generalCategory = OptionsCategory( final generalCategory = OptionsCategory(
name: (final context) => AppLocalizations.of(context).general, name: (final context) => AppLocalizations.of(context).general,
); );
@ -57,11 +55,11 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'default-category', key: 'default-category',
label: (final context) => AppLocalizations.of(context).optionsDefaultCategory, label: (final context) => AppLocalizations.of(context).optionsDefaultCategory,
defaultValue: DefaultCategory.articles, defaultValue: DefaultCategory.articles,
values: BehaviorSubject.seeded({ values: {
DefaultCategory.articles: (final context) => AppLocalizations.of(context).articles, DefaultCategory.articles: (final context) => AppLocalizations.of(context).articles,
DefaultCategory.folders: (final context) => AppLocalizations.of(context).folders, DefaultCategory.folders: (final context) => AppLocalizations.of(context).folders,
DefaultCategory.feeds: (final context) => AppLocalizations.of(context).feeds, DefaultCategory.feeds: (final context) => AppLocalizations.of(context).feeds,
}), },
); );
late final articleViewTypeOption = SelectOption<ArticleViewType>( late final articleViewTypeOption = SelectOption<ArticleViewType>(
@ -70,7 +68,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'article-view-type', key: 'article-view-type',
label: (final context) => AppLocalizations.of(context).optionsArticleViewType, label: (final context) => AppLocalizations.of(context).optionsArticleViewType,
defaultValue: ArticleViewType.direct, defaultValue: ArticleViewType.direct,
values: _articleViewTypeValuesSubject, values: {},
); );
late final articleDisableMarkAsReadTimeoutOption = ToggleOption( late final articleDisableMarkAsReadTimeoutOption = ToggleOption(
@ -87,11 +85,11 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'default-articles-filter', key: 'default-articles-filter',
label: (final context) => AppLocalizations.of(context).optionsDefaultArticlesFilter, label: (final context) => AppLocalizations.of(context).optionsDefaultArticlesFilter,
defaultValue: FilterType.unread, defaultValue: FilterType.unread,
values: BehaviorSubject.seeded({ values: {
FilterType.all: (final context) => AppLocalizations.of(context).articlesFilterAll, FilterType.all: (final context) => AppLocalizations.of(context).articlesFilterAll,
FilterType.unread: (final context) => AppLocalizations.of(context).articlesFilterUnread, FilterType.unread: (final context) => AppLocalizations.of(context).articlesFilterUnread,
FilterType.starred: (final context) => AppLocalizations.of(context).articlesFilterStarred, FilterType.starred: (final context) => AppLocalizations.of(context).articlesFilterStarred,
}), },
); );
late final articlesSortPropertyOption = SelectOption<ArticlesSortProperty>( late final articlesSortPropertyOption = SelectOption<ArticlesSortProperty>(
@ -100,13 +98,13 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'articles-sort-property', key: 'articles-sort-property',
label: (final context) => AppLocalizations.of(context).optionsArticlesSortProperty, label: (final context) => AppLocalizations.of(context).optionsArticlesSortProperty,
defaultValue: ArticlesSortProperty.publishDate, defaultValue: ArticlesSortProperty.publishDate,
values: BehaviorSubject.seeded({ values: {
ArticlesSortProperty.publishDate: (final context) => ArticlesSortProperty.publishDate: (final context) =>
AppLocalizations.of(context).optionsArticlesSortPropertyPublishDate, AppLocalizations.of(context).optionsArticlesSortPropertyPublishDate,
ArticlesSortProperty.alphabetical: (final context) => ArticlesSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsArticlesSortPropertyAlphabetical, AppLocalizations.of(context).optionsArticlesSortPropertyAlphabetical,
ArticlesSortProperty.byFeed: (final context) => AppLocalizations.of(context).optionsArticlesSortPropertyFeed, ArticlesSortProperty.byFeed: (final context) => AppLocalizations.of(context).optionsArticlesSortPropertyFeed,
}), },
); );
late final articlesSortBoxOrderOption = SelectOption<SortBoxOrder>( late final articlesSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -115,7 +113,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'articles-sort-box-order', key: 'articles-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsArticlesSortOrder, label: (final context) => AppLocalizations.of(context).optionsArticlesSortOrder,
defaultValue: SortBoxOrder.descending, defaultValue: SortBoxOrder.descending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues), values: sortBoxOrderOptionValues,
); );
late final foldersSortPropertyOption = SelectOption<FoldersSortProperty>( late final foldersSortPropertyOption = SelectOption<FoldersSortProperty>(
@ -124,12 +122,12 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'folders-sort-property', key: 'folders-sort-property',
label: (final context) => AppLocalizations.of(context).optionsFoldersSortProperty, label: (final context) => AppLocalizations.of(context).optionsFoldersSortProperty,
defaultValue: FoldersSortProperty.alphabetical, defaultValue: FoldersSortProperty.alphabetical,
values: BehaviorSubject.seeded({ values: {
FoldersSortProperty.alphabetical: (final context) => FoldersSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsFoldersSortPropertyAlphabetical, AppLocalizations.of(context).optionsFoldersSortPropertyAlphabetical,
FoldersSortProperty.unreadCount: (final context) => FoldersSortProperty.unreadCount: (final context) =>
AppLocalizations.of(context).optionsFoldersSortPropertyUnreadCount, AppLocalizations.of(context).optionsFoldersSortPropertyUnreadCount,
}), },
); );
late final foldersSortBoxOrderOption = SelectOption<SortBoxOrder>( late final foldersSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -138,7 +136,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'folders-sort-box-order', key: 'folders-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsFoldersSortOrder, label: (final context) => AppLocalizations.of(context).optionsFoldersSortOrder,
defaultValue: SortBoxOrder.ascending, defaultValue: SortBoxOrder.ascending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues), values: sortBoxOrderOptionValues,
); );
late final defaultFolderViewTypeOption = SelectOption<DefaultFolderViewType>( late final defaultFolderViewTypeOption = SelectOption<DefaultFolderViewType>(
@ -147,10 +145,10 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'default-folder-view-type', key: 'default-folder-view-type',
label: (final context) => AppLocalizations.of(context).optionsDefaultFolderViewType, label: (final context) => AppLocalizations.of(context).optionsDefaultFolderViewType,
defaultValue: DefaultFolderViewType.articles, defaultValue: DefaultFolderViewType.articles,
values: BehaviorSubject.seeded({ values: {
DefaultFolderViewType.articles: (final context) => AppLocalizations.of(context).articles, DefaultFolderViewType.articles: (final context) => AppLocalizations.of(context).articles,
DefaultFolderViewType.feeds: (final context) => AppLocalizations.of(context).feeds, DefaultFolderViewType.feeds: (final context) => AppLocalizations.of(context).feeds,
}), },
); );
late final feedsSortPropertyOption = SelectOption<FeedsSortProperty>( late final feedsSortPropertyOption = SelectOption<FeedsSortProperty>(
@ -159,12 +157,12 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'feeds-sort-property', key: 'feeds-sort-property',
label: (final context) => AppLocalizations.of(context).optionsFeedsSortProperty, label: (final context) => AppLocalizations.of(context).optionsFeedsSortProperty,
defaultValue: FeedsSortProperty.alphabetical, defaultValue: FeedsSortProperty.alphabetical,
values: BehaviorSubject.seeded({ values: {
FeedsSortProperty.alphabetical: (final context) => FeedsSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsFeedsSortPropertyAlphabetical, AppLocalizations.of(context).optionsFeedsSortPropertyAlphabetical,
FeedsSortProperty.unreadCount: (final context) => FeedsSortProperty.unreadCount: (final context) =>
AppLocalizations.of(context).optionsFeedsSortPropertyUnreadCount, AppLocalizations.of(context).optionsFeedsSortPropertyUnreadCount,
}), },
); );
late final feedsSortBoxOrderOption = SelectOption<SortBoxOrder>( late final feedsSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -173,7 +171,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'feeds-sort-box-order', key: 'feeds-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsFeedsSortOrder, label: (final context) => AppLocalizations.of(context).optionsFeedsSortOrder,
defaultValue: SortBoxOrder.ascending, defaultValue: SortBoxOrder.ascending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues), values: sortBoxOrderOptionValues,
); );
} }

4
packages/neon/neon_news/lib/widgets/folder_view.dart

@ -26,11 +26,11 @@ class _NewsFolderViewState extends State<NewsFolderView> {
child: DropdownButton<DefaultFolderViewType>( child: DropdownButton<DefaultFolderViewType>(
isExpanded: true, isExpanded: true,
value: _viewType, value: _viewType,
items: option.values.value.keys items: option.values.keys
.map( .map(
(final key) => DropdownMenuItem<DefaultFolderViewType>( (final key) => DropdownMenuItem<DefaultFolderViewType>(
value: key, value: key,
child: Text(option.values.value[key]!(context)), child: Text(option.values[key]!(context)),
), ),
) )
.toList(), .toList(),

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

@ -35,10 +35,10 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
key: 'default-category', key: 'default-category',
label: (final context) => AppLocalizations.of(context).optionsDefaultCategory, label: (final context) => AppLocalizations.of(context).optionsDefaultCategory,
defaultValue: DefaultCategory.notes, defaultValue: DefaultCategory.notes,
values: BehaviorSubject.seeded({ values: {
DefaultCategory.notes: (final context) => AppLocalizations.of(context).notes, DefaultCategory.notes: (final context) => AppLocalizations.of(context).notes,
DefaultCategory.categories: (final context) => AppLocalizations.of(context).categories, DefaultCategory.categories: (final context) => AppLocalizations.of(context).categories,
}), },
); );
late final defaultNoteViewTypeOption = SelectOption<DefaultNoteViewType>( late final defaultNoteViewTypeOption = SelectOption<DefaultNoteViewType>(
@ -47,10 +47,10 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
key: 'default-note-view-type', key: 'default-note-view-type',
label: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewType, label: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewType,
defaultValue: DefaultNoteViewType.preview, defaultValue: DefaultNoteViewType.preview,
values: BehaviorSubject.seeded({ values: {
DefaultNoteViewType.preview: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewTypePreview, DefaultNoteViewType.preview: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewTypePreview,
DefaultNoteViewType.edit: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewTypeEdit, DefaultNoteViewType.edit: (final context) => AppLocalizations.of(context).optionsDefaultNoteViewTypeEdit,
}), },
); );
late final notesSortPropertyOption = SelectOption<NotesSortProperty>( late final notesSortPropertyOption = SelectOption<NotesSortProperty>(
@ -59,12 +59,12 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
key: 'notes-sort-property', key: 'notes-sort-property',
label: (final context) => AppLocalizations.of(context).optionsNotesSortProperty, label: (final context) => AppLocalizations.of(context).optionsNotesSortProperty,
defaultValue: NotesSortProperty.lastModified, defaultValue: NotesSortProperty.lastModified,
values: BehaviorSubject.seeded({ values: {
NotesSortProperty.lastModified: (final context) => NotesSortProperty.lastModified: (final context) =>
AppLocalizations.of(context).optionsNotesSortPropertyLastModified, AppLocalizations.of(context).optionsNotesSortPropertyLastModified,
NotesSortProperty.alphabetical: (final context) => NotesSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsNotesSortPropertyAlphabetical, AppLocalizations.of(context).optionsNotesSortPropertyAlphabetical,
}), },
); );
late final notesSortBoxOrderOption = SelectOption<SortBoxOrder>( late final notesSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -73,7 +73,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
key: 'notes-sort-box-order', key: 'notes-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsNotesSortOrder, label: (final context) => AppLocalizations.of(context).optionsNotesSortOrder,
defaultValue: SortBoxOrder.descending, defaultValue: SortBoxOrder.descending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues), values: sortBoxOrderOptionValues,
); );
late final categoriesSortPropertyOption = SelectOption<CategoriesSortProperty>( late final categoriesSortPropertyOption = SelectOption<CategoriesSortProperty>(
@ -82,12 +82,12 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
key: 'categories-sort-property', key: 'categories-sort-property',
label: (final context) => AppLocalizations.of(context).optionsCategoriesSortProperty, label: (final context) => AppLocalizations.of(context).optionsCategoriesSortProperty,
defaultValue: CategoriesSortProperty.alphabetical, defaultValue: CategoriesSortProperty.alphabetical,
values: BehaviorSubject.seeded({ values: {
CategoriesSortProperty.alphabetical: (final context) => CategoriesSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsCategoriesSortPropertyAlphabetical, AppLocalizations.of(context).optionsCategoriesSortPropertyAlphabetical,
CategoriesSortProperty.notesCount: (final context) => CategoriesSortProperty.notesCount: (final context) =>
AppLocalizations.of(context).optionsCategoriesSortPropertyNotesCount, AppLocalizations.of(context).optionsCategoriesSortPropertyNotesCount,
}), },
); );
late final categoriesSortBoxOrderOption = SelectOption<SortBoxOrder>( late final categoriesSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -96,7 +96,7 @@ class NotesAppSpecificOptions extends NextcloudAppOptions {
key: 'categories-sort-box-order', key: 'categories-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsCategoriesSortOrder, label: (final context) => AppLocalizations.of(context).optionsCategoriesSortOrder,
defaultValue: SortBoxOrder.ascending, defaultValue: SortBoxOrder.ascending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues), values: sortBoxOrderOptionValues,
); );
} }

Loading…
Cancel
Save