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

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

@ -89,18 +89,14 @@ class AccountsBloc extends Bloc implements AccountsBlocEvents, AccountsBlocState
}
}
unawaited(
_globalOptions.initialAccount.stream.first.then((final lastAccount) {
final account = as.tryFind(lastAccount);
if (activeAccount.valueOrNull == null) {
if (account != null) {
setActiveAccount(account);
} else if (as.isNotEmpty) {
setActiveAccount(as.first);
}
}
}),
);
final account = as.tryFind(_globalOptions.initialAccount.value);
if (activeAccount.valueOrNull == null) {
if (account != null) {
setActiveAccount(account);
} else if (as.isNotEmpty) {
setActiveAccount(as.first);
}
}
}
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))));
});
appImplementations.listen((final result) {
appImplementations.listen((final result) async {
if (!result.hasData) {
return;
}
final options = _accountsBloc.getOptionsFor(_account);
unawaited(
options.initialApp.stream.first.then((var initialApp) async {
initialApp ??= _getInitialAppFallback();
if (!activeApp.hasValue && initialApp != null) {
await setActiveApp(initialApp);
}
}),
);
final initialApp = options.initialApp.value ?? _getInitialAppFallback();
if (!activeApp.hasValue && initialApp != null) {
await setActiveApp(initialApp);
}
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([
_globalOptions.pushNotificationsEnabled.stream,
_globalOptions.pushNotificationsDistributor.stream,
_globalOptions.pushNotificationsDistributor.values,
_accountsBloc.accounts,
]).debounceTime(const Duration(milliseconds: 100)).listen((final _) async {
if (!_globalOptions.pushNotificationsEnabled.enabled.hasValue ||
!_globalOptions.pushNotificationsEnabled.enabled.value ||
!_globalOptions.pushNotificationsEnabled.hasValue ||
!_globalOptions.pushNotificationsEnabled.value) {
if (!_globalOptions.pushNotificationsEnabled.enabled || !_globalOptions.pushNotificationsEnabled.value) {
return;
}
if (_globalOptions.pushNotificationsDistributor.value != null) {
return;
}
if (_globalOptions.pushNotificationsDistributor.values.value.containsKey(unifiedPushNextPushID)) {
if (_globalOptions.pushNotificationsDistributor.values.containsKey(unifiedPushNextPushID)) {
// NextPush is already installed
return;
}

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

@ -32,16 +32,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
if (_platform.canUsePushNotifications) {
unawaited(UnifiedPush.getDistributors().then(_globalOptions.updateDistributors));
_globalOptions.pushNotificationsEnabled.stream.listen((final enabled) async {
if (enabled != _pushNotificationsEnabled) {
_pushNotificationsEnabled = enabled;
if (enabled) {
// We just use a single RSA keypair for all accounts
_keypair = await PushUtils.loadRSAKeypair(_storage);
await _setupUnifiedPush();
}
}
});
_globalOptions.pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener);
}
}
@ -50,14 +41,15 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
final SharedPreferences _sharedPreferences;
late final _storage = AppStorage('notifications', _sharedPreferences);
final GlobalOptions _globalOptions;
late RSAKeypair _keypair;
bool? _pushNotificationsEnabled;
final _notificationsController = StreamController<PushNotification>();
StreamSubscription? _accountsListener;
@override
void dispose() {
unawaited(_notificationsController.close());
unawaited(_accountsListener?.cancel());
_globalOptions.pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener);
}
@override
@ -65,7 +57,22 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
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 {
// We just use a single RSA keypair for all accounts
final keypair = await PushUtils.loadRSAKeypair(_storage);
await UnifiedPush.initialize(
onNewEndpoint: (final endpoint, final instance) async {
final account = _accountsBloc.accounts.value.tryFind(instance);
@ -83,7 +90,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
final subscription = await account.client.notifications.registerDevice(
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
);
@ -95,24 +102,23 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
},
onMessage: PushUtils.onMessage,
);
}
_globalOptions.pushNotificationsDistributor.stream.listen((final distributor) async {
final disabled = distributor == null;
final sameDistributor = distributor == await UnifiedPush.getDistributor();
final accounts = _accountsBloc.accounts.value;
if (disabled || !sameDistributor) {
await _unregisterUnifiedPushInstances(accounts);
}
if (!disabled && !sameDistributor) {
debugPrint('UnifiedPush distributor changed to $distributor');
await UnifiedPush.saveDistributor(distributor);
}
if (!disabled) {
await _registerUnifiedPushInstances(accounts);
}
});
_accountsBloc.accounts.listen(_registerUnifiedPushInstances);
Future<void> _distributorListener() async {
final distributor = _globalOptions.pushNotificationsDistributor.value;
final disabled = distributor == null;
final sameDistributor = distributor == await UnifiedPush.getDistributor();
final accounts = _accountsBloc.accounts.value;
if (disabled || !sameDistributor) {
await _unregisterUnifiedPushInstances(accounts);
}
if (!disabled && !sameDistributor) {
debugPrint('UnifiedPush distributor changed to $distributor');
await UnifiedPush.saveDistributor(distributor);
}
if (!disabled) {
await _registerUnifiedPushInstances(accounts);
}
}
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,
AppLocalizations.of(context).settingsResetForConfirmation(name),
)) {
await options.reset();
options.reset();
}
},
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/models/account.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' as global_options;
import 'package:neon/src/utils/global_popups.dart';
@ -150,9 +149,9 @@ class _HomePageState extends State<HomePage> {
},
);
final body = OptionBuilder<global_options.NavigationMode>(
option: _globalOptions.navigationMode,
builder: (final context, final navigationMode) {
final body = ValueListenableBuilder<global_options.NavigationMode>(
valueListenable: _globalOptions.navigationMode,
builder: (final context, final navigationMode, final _) {
final drawerAlwaysVisible = navigationMode == global_options.NavigationMode.drawerAlwaysVisible;
final body = Scaffold(

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

@ -30,7 +30,7 @@ class NextcloudAppSettingsPage extends StatelessWidget {
context,
AppLocalizations.of(context).settingsResetForConfirmation(appImplementation.name(context)),
)) {
await appImplementation.options.reset();
appImplementation.options.reset();
}
},
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(
onPressed: () async {
if (await showConfirmationDialog(context, AppLocalizations.of(context).settingsResetAllConfirmation)) {
await globalOptions.reset();
globalOptions.reset();
for (final appImplementation in appImplementations) {
await appImplementation.options.reset();
appImplementation.options.reset();
}
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 platform = Provider.of<NeonPlatform>(context, listen: false);
return StreamBuilder<bool>(
stream: globalOptions.pushNotificationsEnabled.enabled,
initialData: globalOptions.pushNotificationsEnabled.enabled.valueOrNull,
return ValueListenableBuilder<bool>(
valueListenable: globalOptions.pushNotificationsEnabled,
builder: (
final context,
final pushNotificationsEnabledEnabledSnapshot,
final _,
final __,
) =>
SettingsList(
initialCategory: widget.initialCategory?.name,
@ -144,8 +144,7 @@ class _SettingsPageState extends State<SettingsPage> {
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
key: ValueKey(SettingsCageories.pushNotifications.name),
tiles: [
if (pushNotificationsEnabledEnabledSnapshot.hasData &&
!pushNotificationsEnabledEnabledSnapshot.requireData) ...[
if (!globalOptions.pushNotificationsEnabled.enabled) ...[
TextSettingsTile(
text: AppLocalizations.of(context).globalOptionsPushNotificationsEnabledDisabledNotice,
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<Option> options;
Future reset() async {
void reset() {
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 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/options_category.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';
@internal
class OptionDisableException implements Exception {}
@immutable
@internal
abstract class Option<T> {
abstract class Option<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates an Option
Option({
required this.storage,
required this.key,
required this.label,
required this.defaultValue,
required this.stream,
final bool enabled = true,
this.category,
final BehaviorSubject<bool>? enabled,
}) : enabled = enabled ?? BehaviorSubject<bool>.seeded(true);
final T? initialValue,
}) : _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 String key;
final LabelBuilder label;
final T defaultValue;
final OptionsCategory? category;
final BehaviorSubject<bool> enabled;
final BehaviorSubject<T> stream;
T get value {
if (hasValue) {
return stream.value;
}
T _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 {
if (!enabled.value) {
throw OptionDisableException();
}
bool _enabled;
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 {
await set(defaultValue);
/// Resets the option to its [default] value.
void reset() {
value = defaultValue;
}
void dispose() {
unawaited(stream.close());
unawaited(enabled.close());
}
/// Deserializes the data.
T deserialize(final Object data);
/// 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 'package:collection/collection.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:rxdart/rxdart.dart';
class SelectOption<T> extends Option<T> {
/// Creates a SelectOption
SelectOption({
required super.storage,
required super.key,
required super.label,
required super.defaultValue,
required this.values,
required final Map<T, LabelBuilder> values,
super.category,
super.enabled,
}) : super(stream: BehaviorSubject()) {
unawaited(
values.first.then((final vs) async {
final valueStr = storage.getString(key);
T? initialValue;
if (valueStr != null) {
initialValue = _fromString(vs, valueStr);
}
stream.add(initialValue ?? defaultValue);
}),
);
}
}) : _values = values,
super(initialValue: _fromString(values, storage.getString(key)));
/// Creates a SelectOption depending on the State of another [Option].
SelectOption.depend({
required super.storage,
required super.key,
required super.label,
required super.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) {
final v = vs.keys.where((final e) => e.toString() == valueStr);
if (v.length == 1) {
return v.first;
static T? _fromString<T>(final Map<T, LabelBuilder> vs, final String? valueStr) {
if (valueStr == null) {
return null;
}
return null;
return vs.keys.firstWhereOrNull((final e) => e.toString() == valueStr);
}
final BehaviorSubject<Map<T, LabelBuilder>> values;
Map<T, LabelBuilder> _values;
@override
Future set(final T value) {
stream.add(value);
return storage.setString(key, value.toString());
set value(final T value) {
super.value = value;
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
String? serialize() => value?.toString();
String serialize() => value.toString();
@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:rxdart/rxdart.dart';
class ToggleOption extends Option<bool> {
/// Creates a ToggleOption
ToggleOption({
required super.storage,
required super.key,
required super.label,
required super.defaultValue,
required final bool defaultValue,
super.category,
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
Future set(final bool value) {
stream.add(value);
return storage.setBool(key, value);
set value(final bool value) {
super.value = value;
unawaited(storage.setBool(key, serialize()));
}
@override
bool serialize() => value;
@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:meta/meta.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';
@internal
@ -12,21 +11,16 @@ class CheckBoxSettingsTile extends InputSettingsTile<ToggleOption> {
});
@override
Widget build(final BuildContext context) => OptionBuilder<bool>(
option: option,
builder: (final context, final value) => StreamBuilder<bool>(
stream: option.enabled,
builder: (final context, final enabledSnapshot) => !enabledSnapshot.hasData
? const SizedBox()
: CheckboxListTile(
title: Text(option.label(context)),
value: value,
onChanged: enabledSnapshot.requireData
? (final value) async {
await option.set(value!);
}
: null,
),
Widget build(final BuildContext context) => ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) => CheckboxListTile.adaptive(
enabled: option.enabled,
title: child,
value: value,
onChanged: (final value) {
option.value = value!;
},
),
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:meta/meta.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';
@internal
@ -13,64 +11,43 @@ class DropdownButtonSettingsTile<T> extends InputSettingsTile<SelectOption<T>> {
});
@override
Widget build(final BuildContext context) => OptionBuilder<T>(
option: option,
builder: (
final context,
final value,
) =>
StreamBuilder<bool>(
stream: option.enabled,
builder: (
final context,
final enabledSnapshot,
) =>
StreamBuilder<Map<T, LabelBuilder>>(
stream: option.values,
builder: (
final context,
final valuesSnapshot,
) =>
LayoutBuilder(
builder: (final context, final constraints) => ListTile(
title: Text(
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,
Widget build(final BuildContext context) => ValueListenableBuilder(
valueListenable: option,
builder: (final context, final value, final child) => LayoutBuilder(
builder: (final context, final constraints) => ListTile(
enabled: option.enabled,
title: child,
trailing: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.5,
),
child: IntrinsicWidth(
child: DropdownButton<T>(
isExpanded: true,
value: value,
items: option.values.keys
.map(
(final k) => DropdownMenuItem(
value: k,
child: Text(
option.values[k]!(context),
overflow: TextOverflow.ellipsis,
),
),
)
: 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:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/widgets/option_builder.dart';
import 'package:sort_box/sort_box.dart';
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 OptionBuilder<T>(
option: sortPropertyOption,
builder: (final context, final property) => OptionBuilder<SortBoxOrder>(
option: sortBoxOrderOption,
builder: (final context, final order) {
return ValueListenableBuilder<T>(
valueListenable: sortPropertyOption,
builder: (final context, final property, final _) => ValueListenableBuilder<SortBoxOrder>(
valueListenable: sortBoxOrderOption,
builder: (final context, final order, final _) {
final box = Box(property, order);
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:neon/l10n/localizations.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/select_option.dart';
import 'package:neon/src/settings/models/storage.dart';
import 'package:neon/src/settings/widgets/label_builder.dart';
import 'package:rxdart/rxdart.dart';
@internal
class AccountSpecificOptions {
@ -16,33 +12,33 @@ class AccountSpecificOptions {
this._appsBloc,
) {
_appsBloc.appImplementations.listen((final result) {
if (result.hasData) {
_appIDsSubject.add({
null: (final context) => AppLocalizations.of(context).accountOptionsAutomatic,
for (final app in result.requireData) ...{
app.id: app.name,
},
});
if (!result.hasData) {
return;
}
initialApp.values = {
null: (final context) => AppLocalizations.of(context).accountOptionsAutomatic,
for (final app in result.requireData) ...{
app.id: app.name,
},
};
});
}
final AppStorage _storage;
final AppsBloc _appsBloc;
final _appIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
late final List<Option> options = [
initialApp,
];
Future reset() async {
void reset() {
for (final option in options) {
await option.reset();
option.reset();
}
}
void dispose() {
unawaited(_appIDsSubject.close());
for (final option in options) {
option.dispose();
}
@ -53,6 +49,6 @@ class AccountSpecificOptions {
key: 'initial-app',
label: (final context) => AppLocalizations.of(context).accountOptionsInitialApp,
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/storage.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:permission_handler/permission_handler.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.dart';
const unifiedPushNextPushID = 'org.unifiedpush.distributor.nextpush';
@ -23,54 +21,38 @@ class GlobalOptions {
this._sharedPreferences,
this._packageInfo,
) {
themeMode.stream.listen((final value) {
_themeOLEDAsDarkEnabledSubject.add(value != ThemeMode.light);
});
_pushNotificationsDistributorsSubject.listen((final distributors) async {
final allowed = distributors.isNotEmpty;
_pushNotificationsEnabledEnabledSubject.add(allowed);
if (!allowed) {
await pushNotificationsEnabled.set(false);
}
});
pushNotificationsEnabled.stream.listen((final enabled) async {
if (enabled) {
final response = await Permission.notification.request();
if (response.isPermanentlyDenied) {
_pushNotificationsEnabledEnabledSubject.add(false);
}
if (!response.isGranted) {
await pushNotificationsEnabled.set(false);
}
} else {
await pushNotificationsDistributor.set(null);
pushNotificationsEnabled.addListener(_pushNotificationsEnabledListener);
rememberLastUsedAccount.addListener(_rememberLastUsedAccountListener);
}
void _rememberLastUsedAccountListener() {
initialAccount.enabled = !rememberLastUsedAccount.value;
if (rememberLastUsedAccount.value) {
initialAccount.value = null;
} 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;
}
}
Future<void> _pushNotificationsEnabledListener() async {
if (pushNotificationsEnabled.value) {
final response = await Permission.notification.request();
if (response.isPermanentlyDenied) {
pushNotificationsEnabled.enabled = 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);
}
if (!response.isGranted) {
pushNotificationsEnabled.value = false;
}
});
} else {
pushNotificationsDistributor.value = null;
}
}
final SharedPreferences _sharedPreferences;
late final AppStorage _storage = AppStorage('global', _sharedPreferences);
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)>{
_packageInfo.packageName: (final context) =>
@ -103,37 +85,44 @@ class GlobalOptions {
navigationMode,
];
Future reset() async {
void reset() {
for (final option in options) {
await option.reset();
option.reset();
}
}
void dispose() {
unawaited(_accountsIDsSubject.close());
unawaited(_themeOLEDAsDarkEnabledSubject.close());
for (final option in options) {
option.dispose();
}
pushNotificationsEnabled.removeListener(_pushNotificationsEnabledListener);
rememberLastUsedAccount.removeListener(_rememberLastUsedAccountListener);
}
void updateAccounts(final List<Account> accounts) {
if (accounts.isEmpty) {
return;
}
_accountsIDsSubject.add({
initialAccount.values = {
for (final account in accounts) ...{
account.id: (final context) => account.client.humanReadableID,
},
});
};
}
Future updateDistributors(final List<String> distributors) async {
_pushNotificationsDistributorsSubject.add({
pushNotificationsDistributor.values = {
for (final distributor in distributors) ...{
distributor: _distributorsMap[distributor] ?? (final _) => distributor,
},
});
};
final allowed = distributors.isNotEmpty;
pushNotificationsEnabled.enabled = allowed;
if (!allowed) {
pushNotificationsEnabled.value = false;
}
}
late final themeMode = SelectOption<ThemeMode>(
@ -141,11 +130,11 @@ class GlobalOptions {
key: 'theme-mode',
label: (final context) => AppLocalizations.of(context).globalOptionsThemeMode,
defaultValue: ThemeMode.system,
values: BehaviorSubject.seeded({
values: {
ThemeMode.light: (final context) => AppLocalizations.of(context).globalOptionsThemeModeLight,
ThemeMode.dark: (final context) => AppLocalizations.of(context).globalOptionsThemeModeDark,
ThemeMode.system: (final context) => AppLocalizations.of(context).globalOptionsThemeModeAutomatic,
}),
},
);
late final themeOLEDAsDark = ToggleOption(
@ -153,7 +142,6 @@ class GlobalOptions {
key: 'theme-oled-as-dark',
label: (final context) => AppLocalizations.of(context).globalOptionsThemeOLEDAsDark,
defaultValue: false,
enabled: _themeOLEDAsDarkEnabledSubject,
);
late final themeKeepOriginalAccentColor = ToggleOption(
@ -168,16 +156,15 @@ class GlobalOptions {
key: 'push-notifications-enabled',
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled,
defaultValue: false,
enabled: _pushNotificationsEnabledEnabledSubject,
);
late final pushNotificationsDistributor = SelectOption<String?>(
late final pushNotificationsDistributor = SelectOption<String?>.depend(
storage: _storage,
key: 'push-notifications-distributor',
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsDistributor,
defaultValue: null,
values: _pushNotificationsDistributorsSubject,
enabled: pushNotificationsEnabled.stream,
values: {},
enabled: pushNotificationsEnabled,
);
late final startupMinimized = ToggleOption(
@ -203,12 +190,12 @@ class GlobalOptions {
defaultValue: false,
);
late final systemTrayHideToTrayWhenMinimized = ToggleOption(
late final systemTrayHideToTrayWhenMinimized = ToggleOption.depend(
storage: _storage,
key: 'systemtray-hide-to-tray-when-minimized',
label: (final context) => AppLocalizations.of(context).globalOptionsSystemTrayHideToTrayWhenMinimized,
defaultValue: true,
enabled: systemTrayEnabled.stream,
enabled: systemTrayEnabled,
);
late final rememberLastUsedAccount = ToggleOption(
@ -223,8 +210,7 @@ class GlobalOptions {
key: 'initial-account',
label: (final context) => AppLocalizations.of(context).globalOptionsAccountsInitialAccount,
defaultValue: null,
values: _accountsIDsSubject,
enabled: _initialAccountEnabledSubject,
values: {},
);
late final navigationMode = SelectOption<NavigationMode>(
@ -232,7 +218,7 @@ class GlobalOptions {
key: 'navigation-mode',
label: (final context) => AppLocalizations.of(context).globalOptionsNavigationMode,
defaultValue: Platform.isAndroid || Platform.isIOS ? NavigationMode.drawer : NavigationMode.drawerAlwaysVisible,
values: BehaviorSubject.seeded({
values: {
NavigationMode.drawer: (final context) => AppLocalizations.of(context).globalOptionsNavigationModeDrawer,
if (!Platform.isAndroid && !Platform.isIOS) ...{
NavigationMode.drawerAlwaysVisible: (final context) =>
@ -240,7 +226,7 @@ class GlobalOptions {
},
// ignore: deprecated_member_use_from_same_package
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 nextPushBloc = Provider.of<NextPushBloc>(context, listen: false);
firstLaunchBloc.onFirstLaunch.listen((final _) async {
if (await globalOptions.pushNotificationsEnabled.enabled.first) {
// ignore: use_build_context_synchronously
firstLaunchBloc.onFirstLaunch.listen((final _) {
if (globalOptions.pushNotificationsEnabled.enabled) {
if (!context.mounted) {
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 option in options) {
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() => {
'global': {
for (final option in globalOptions.options) ...{
if (option.enabled.value) ...{
if (option.enabled) ...{
option.key: option.serialize(),
},
},
@ -72,7 +76,7 @@ class SettingsExportHelper {
for (final appImplementation in appImplementations) ...{
appImplementation.id: {
for (final option in appImplementation.options.options) ...{
if (option.enabled.value) ...{
if (option.enabled) ...{
option.key: option.serialize(),
},
},
@ -83,7 +87,7 @@ class SettingsExportHelper {
for (final account in accountSpecificOptions.keys) ...{
account.id: {
for (final option in accountSpecificOptions[account]!) ...{
if (option.enabled.value) ...{
if (option.enabled) ...{
option.key: option.serialize(),
},
},

1
packages/neon/neon/pubspec.yaml

@ -60,6 +60,7 @@ dev_dependencies:
build_runner: ^2.4.4
go_router_builder: ^2.2.1
json_serializable: ^6.6.2
mocktail: ^0.3.0
nit_picking:
git:
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._platform,
) {
options.uploadQueueParallelism.stream.listen((final value) {
_uploadQueue.parallel = value;
});
options.downloadQueueParallelism.stream.listen((final value) {
_downloadQueue.parallel = value;
});
options.uploadQueueParallelism.addListener(_uploadParalelismListener);
options.downloadQueueParallelism.addListener(_downloadParalelismListener);
}
final FilesAppSpecificOptions options;
@ -56,6 +52,9 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
_downloadQueue.dispose();
unawaited(uploadTasks.close());
unawaited(downloadTasks.close());
options.uploadQueueParallelism.removeListener(_uploadParalelismListener);
options.downloadQueueParallelism.removeListener(_downloadParalelismListener);
}
@override
@ -196,4 +195,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
}
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',
label: (final context) => AppLocalizations.of(context).optionsFilesSortProperty,
defaultValue: FilesSortProperty.name,
values: BehaviorSubject.seeded({
values: {
FilesSortProperty.name: (final context) => AppLocalizations.of(context).optionsFilesSortPropertyName,
FilesSortProperty.modifiedDate: (final context) =>
AppLocalizations.of(context).optionsFilesSortPropertyModifiedDate,
FilesSortProperty.size: (final context) => AppLocalizations.of(context).optionsFilesSortPropertySize,
}),
},
);
late final filesSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -40,7 +40,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'files-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsFilesSortOrder,
defaultValue: SortBoxOrder.ascending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues),
values: sortBoxOrderOptionValues,
);
late final showPreviewsOption = ToggleOption(
@ -57,11 +57,11 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'upload-queue-parallelism',
label: (final context) => AppLocalizations.of(context).optionsUploadQueueParallelism,
defaultValue: 4,
values: BehaviorSubject.seeded({
values: {
for (var i = 1; i <= 16; i = i * 2) ...{
i: (final _) => i.toString(),
},
}),
},
);
late final downloadQueueParallelism = SelectOption<int>(
@ -70,11 +70,11 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'download-queue-parallelism',
label: (final context) => AppLocalizations.of(context).optionsDownloadQueueParallelism,
defaultValue: 4,
values: BehaviorSubject.seeded({
values: {
for (var i = 1; i <= 16; i = i * 2) ...{
i: (final _) => i.toString(),
},
}),
},
);
late final _sizeWarningValues = <int?, String Function(BuildContext)>{
@ -100,7 +100,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'upload-size-warning',
label: (final context) => AppLocalizations.of(context).optionsUploadSizeWarning,
defaultValue: _mb(10),
values: BehaviorSubject.seeded(_sizeWarningValues),
values: _sizeWarningValues,
);
late final downloadSizeWarning = SelectOption<int?>(
@ -109,7 +109,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions {
key: 'download-size-warning',
label: (final context) => AppLocalizations.of(context).optionsDownloadSizeWarning,
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>(
option: bloc.options.showPreviewsOption,
builder: (final context, final showPreviewsSnapshot) {
if (showPreviewsSnapshot && (details.hasPreview ?? false)) {
return ValueListenableBuilder<bool>(
valueListenable: bloc.options.showPreviewsOption,
builder: (final context, final showPreviews, final child) {
if (showPreviews && (details.hasPreview ?? false)) {
final account = Provider.of<AccountsBloc>(context, listen: false).activeAccount.value!;
final child = FilePreviewImage(
final preview = FilePreviewImage(
account: account,
file: details,
size: size,
);
if (withBackground) {
return NeonImageWrapper(
borderRadius: borderRadius,
child: child,
child: preview,
);
}
return child;
return preview;
}
return FileIcon(
details.name,
color: color,
size: size.shortestSide,
);
return child!;
},
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,
];
_articleViewTypeValuesSubject.add({
articleViewTypeOption.values = {
ArticleViewType.direct: (final context) => AppLocalizations.of(context).optionsArticleViewTypeDirect,
if (platform.canUseWebView) ...{
ArticleViewType.internalBrowser: (final context) =>
@ -30,11 +30,9 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
},
ArticleViewType.externalBrowser: (final context) =>
AppLocalizations.of(context).optionsArticleViewTypeExternalBrowser,
});
};
}
final _articleViewTypeValuesSubject = BehaviorSubject<Map<ArticleViewType, String Function(BuildContext)>>();
final generalCategory = OptionsCategory(
name: (final context) => AppLocalizations.of(context).general,
);
@ -57,11 +55,11 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'default-category',
label: (final context) => AppLocalizations.of(context).optionsDefaultCategory,
defaultValue: DefaultCategory.articles,
values: BehaviorSubject.seeded({
values: {
DefaultCategory.articles: (final context) => AppLocalizations.of(context).articles,
DefaultCategory.folders: (final context) => AppLocalizations.of(context).folders,
DefaultCategory.feeds: (final context) => AppLocalizations.of(context).feeds,
}),
},
);
late final articleViewTypeOption = SelectOption<ArticleViewType>(
@ -70,7 +68,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'article-view-type',
label: (final context) => AppLocalizations.of(context).optionsArticleViewType,
defaultValue: ArticleViewType.direct,
values: _articleViewTypeValuesSubject,
values: {},
);
late final articleDisableMarkAsReadTimeoutOption = ToggleOption(
@ -87,11 +85,11 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'default-articles-filter',
label: (final context) => AppLocalizations.of(context).optionsDefaultArticlesFilter,
defaultValue: FilterType.unread,
values: BehaviorSubject.seeded({
values: {
FilterType.all: (final context) => AppLocalizations.of(context).articlesFilterAll,
FilterType.unread: (final context) => AppLocalizations.of(context).articlesFilterUnread,
FilterType.starred: (final context) => AppLocalizations.of(context).articlesFilterStarred,
}),
},
);
late final articlesSortPropertyOption = SelectOption<ArticlesSortProperty>(
@ -100,13 +98,13 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'articles-sort-property',
label: (final context) => AppLocalizations.of(context).optionsArticlesSortProperty,
defaultValue: ArticlesSortProperty.publishDate,
values: BehaviorSubject.seeded({
values: {
ArticlesSortProperty.publishDate: (final context) =>
AppLocalizations.of(context).optionsArticlesSortPropertyPublishDate,
ArticlesSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsArticlesSortPropertyAlphabetical,
ArticlesSortProperty.byFeed: (final context) => AppLocalizations.of(context).optionsArticlesSortPropertyFeed,
}),
},
);
late final articlesSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -115,7 +113,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'articles-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsArticlesSortOrder,
defaultValue: SortBoxOrder.descending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues),
values: sortBoxOrderOptionValues,
);
late final foldersSortPropertyOption = SelectOption<FoldersSortProperty>(
@ -124,12 +122,12 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'folders-sort-property',
label: (final context) => AppLocalizations.of(context).optionsFoldersSortProperty,
defaultValue: FoldersSortProperty.alphabetical,
values: BehaviorSubject.seeded({
values: {
FoldersSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsFoldersSortPropertyAlphabetical,
FoldersSortProperty.unreadCount: (final context) =>
AppLocalizations.of(context).optionsFoldersSortPropertyUnreadCount,
}),
},
);
late final foldersSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -138,7 +136,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'folders-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsFoldersSortOrder,
defaultValue: SortBoxOrder.ascending,
values: BehaviorSubject.seeded(sortBoxOrderOptionValues),
values: sortBoxOrderOptionValues,
);
late final defaultFolderViewTypeOption = SelectOption<DefaultFolderViewType>(
@ -147,10 +145,10 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'default-folder-view-type',
label: (final context) => AppLocalizations.of(context).optionsDefaultFolderViewType,
defaultValue: DefaultFolderViewType.articles,
values: BehaviorSubject.seeded({
values: {
DefaultFolderViewType.articles: (final context) => AppLocalizations.of(context).articles,
DefaultFolderViewType.feeds: (final context) => AppLocalizations.of(context).feeds,
}),
},
);
late final feedsSortPropertyOption = SelectOption<FeedsSortProperty>(
@ -159,12 +157,12 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'feeds-sort-property',
label: (final context) => AppLocalizations.of(context).optionsFeedsSortProperty,
defaultValue: FeedsSortProperty.alphabetical,
values: BehaviorSubject.seeded({
values: {
FeedsSortProperty.alphabetical: (final context) =>
AppLocalizations.of(context).optionsFeedsSortPropertyAlphabetical,
FeedsSortProperty.unreadCount: (final context) =>
AppLocalizations.of(context).optionsFeedsSortPropertyUnreadCount,
}),
},
);
late final feedsSortBoxOrderOption = SelectOption<SortBoxOrder>(
@ -173,7 +171,7 @@ class NewsAppSpecificOptions extends NextcloudAppOptions {
key: 'feeds-sort-box-order',
label: (final context) => AppLocalizations.of(context).optionsFeedsSortOrder,
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>(
isExpanded: true,
value: _viewType,
items: option.values.value.keys
items: option.values.keys
.map(
(final key) => DropdownMenuItem<DefaultFolderViewType>(
value: key,
child: Text(option.values.value[key]!(context)),
child: Text(option.values[key]!(context)),
),
)
.toList(),

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

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

Loading…
Cancel
Save