From 9efa2f337d2e312a0b3b9a50e6e099ff8b78c65b Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 20 Oct 2023 12:00:25 +0200 Subject: [PATCH] feat(neon): add optons collection builder Signed-off-by: Nikolas Rimikis --- packages/neon/neon/lib/src/app.dart | 75 +++++++++---------- .../settings/models/options_collection.dart | 5 +- .../widgets/options_collection_builder.dart | 71 ++++++++++++++++++ 3 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 packages/neon/neon/lib/src/widgets/options_collection_builder.dart diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index ce2ce109..5513f751 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -22,6 +22,7 @@ import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/localizations.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/utils/push_utils.dart'; +import 'package:neon/src/widgets/options_collection_builder.dart'; import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/nextcloud.dart'; import 'package:quick_actions/quick_actions.dart'; @@ -275,50 +276,42 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra } @override - 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( - stream: _accountsBloc.activeAccount, - builder: (final context, final activeAccountSnapshot) { - FlutterNativeSplash.remove(); - return ResultBuilder.behaviorSubject( - stream: activeAccountSnapshot.hasData - ? _accountsBloc.getCapabilitiesBlocFor(activeAccountSnapshot.data!).capabilities - : null, - builder: (final context, final capabilitiesSnapshot) { - final appTheme = AppTheme( - capabilitiesSnapshot.data?.capabilities.themingPublicCapabilities?.theming, - keepOriginalAccentColor: themeKeepOriginalAccentColor, - oledAsDark: themeOLEDAsDark, - appThemes: _appImplementations.map((final a) => a.theme).whereNotNull(), - neonTheme: widget.neonTheme, - ); - - return MaterialApp.router( - localizationsDelegates: [ - ..._appImplementations.map((final app) => app.localizationsDelegate), - ...NeonLocalizations.localizationsDelegates, - ], - supportedLocales: { - ..._appImplementations - .map((final app) => app.supportedLocales) - .expand((final element) => element), - ...NeonLocalizations.supportedLocales, - }, - themeMode: themeMode, - theme: appTheme.lightTheme, - darkTheme: appTheme.darkTheme, - routerConfig: _routerDelegate, - ); + Widget build(final BuildContext context) => OptionsCollectionBuilder( + valueListenable: _globalOptions, + builder: (final context, final options, final _) => StreamBuilder( + stream: _accountsBloc.activeAccount, + builder: (final context, final activeAccountSnapshot) { + FlutterNativeSplash.remove(); + return ResultBuilder.behaviorSubject( + stream: activeAccountSnapshot.hasData + ? _accountsBloc.getCapabilitiesBlocFor(activeAccountSnapshot.data!).capabilities + : null, + builder: (final context, final capabilitiesSnapshot) { + final appTheme = AppTheme( + capabilitiesSnapshot.data?.capabilities.themingPublicCapabilities?.theming, + keepOriginalAccentColor: options.themeKeepOriginalAccentColor.value, + oledAsDark: options.themeOLEDAsDark.value, + appThemes: _appImplementations.map((final a) => a.theme).whereNotNull(), + neonTheme: widget.neonTheme, + ); + + return MaterialApp.router( + localizationsDelegates: [ + ..._appImplementations.map((final app) => app.localizationsDelegate), + ...NeonLocalizations.localizationsDelegates, + ], + supportedLocales: { + ..._appImplementations.map((final app) => app.supportedLocales).expand((final element) => element), + ...NeonLocalizations.supportedLocales, }, + themeMode: options.themeMode.value, + theme: appTheme.lightTheme, + darkTheme: appTheme.darkTheme, + routerConfig: _routerDelegate, ); }, - ), - ), + ); + }, ), ); } diff --git a/packages/neon/neon/lib/src/settings/models/options_collection.dart b/packages/neon/neon/lib/src/settings/models/options_collection.dart index f62c4fdb..37b99377 100644 --- a/packages/neon/neon/lib/src/settings/models/options_collection.dart +++ b/packages/neon/neon/lib/src/settings/models/options_collection.dart @@ -1,4 +1,4 @@ -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'package:neon/src/models/disposable.dart'; import 'package:neon/src/settings/models/exportable.dart'; import 'package:neon/src/settings/models/option.dart'; @@ -17,6 +17,9 @@ abstract class OptionsCollection implements Exportable, Disposable { @protected Iterable> get options; + /// Return a [Listenable] that triggers when any of the given [options] themselves trigger. + Listenable get listenable => Listenable.merge(options.toList()); + /// Resets all [options]. /// /// Implementers extending this must call super. diff --git a/packages/neon/neon/lib/src/widgets/options_collection_builder.dart b/packages/neon/neon/lib/src/widgets/options_collection_builder.dart new file mode 100644 index 00000000..a59c12bb --- /dev/null +++ b/packages/neon/neon/lib/src/widgets/options_collection_builder.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:neon/src/settings/models/options_collection.dart'; + +/// A widget that rebuilds when one of the options in an [OptionsCollection] changes. +class OptionsCollectionBuilder extends StatefulWidget { + /// Creates a [OptionsCollectionBuilder]. + /// + /// The [valueListenable] and [builder] arguments must not be null. + /// The [child] is optional but is good practice to use if part of the widget + /// subtree does not depend on the value of the [valueListenable]. + const OptionsCollectionBuilder({ + required this.valueListenable, + required this.builder, + this.child, + super.key, + }); + + /// The [OptionsCollection] whose values you depend on in order to build. + final T valueListenable; + + /// A [ValueWidgetBuilder] which builds a widget depending on the + /// [valueListenable]'s value. + /// + /// Can incorporate a [valueListenable] value-independent widget subtree + /// from the [child] parameter into the returned widget tree. + /// + /// Must not be null. + final ValueWidgetBuilder builder; + + /// A [valueListenable]-independent widget which is passed back to the [builder]. + /// + /// This argument is optional and can be null if the entire widget subtree the + /// [builder] builds depends on the value of the [valueListenable]. For + /// example, in the case where the [valueListenable] is a [String] and the + /// [builder] returns a [Text] widget with the current [String] value, there + /// would be no useful [child]. + final Widget? child; + + @override + State createState() => _OptionsCollectionBuilderState(); +} + +class _OptionsCollectionBuilderState extends State> { + @override + void initState() { + super.initState(); + widget.valueListenable.listenable.addListener(_valueChanged); + } + + @override + void didUpdateWidget(final OptionsCollectionBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.valueListenable != widget.valueListenable) { + oldWidget.valueListenable.listenable.removeListener(_valueChanged); + widget.valueListenable.listenable.addListener(_valueChanged); + } + } + + @override + void dispose() { + widget.valueListenable.listenable.removeListener(_valueChanged); + super.dispose(); + } + + void _valueChanged() { + setState(() {}); + } + + @override + Widget build(final BuildContext context) => widget.builder(context, widget.valueListenable, widget.child); +}