From 6cf9cd0f0cf99eb1963a997590ad16a0ad5e5179 Mon Sep 17 00:00:00 2001 From: Ahmed Hnewa <73608287+freshtechtips@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:44:48 +0300 Subject: [PATCH] Major update 6 (#1456) --- CHANGELOG.md | 7 + example/lib/pages/home_page.dart | 12 +- .../lib/embeds/toolbar/camera_button.dart | 2 +- .../lib/embeds/toolbar/formula_button.dart | 2 +- .../lib/embeds/toolbar/image_button.dart | 2 +- .../lib/embeds/toolbar/media_button.dart | 2 +- .../lib/embeds/toolbar/video_button.dart | 2 +- flutter_quill_extensions/pubspec.yaml | 2 +- .../models/config/shared_configurations.dart | 7 + .../config/toolbar/buttons/link_style.dart | 39 ++++ .../models/config/toolbar/buttons/search.dart | 61 ++++++ .../toolbar/buttons/select_header_style.dart | 41 ++++ .../models/config/toolbar/configurations.dart | 31 ++- .../models/themes/quill_custom_button.dart | 16 +- lib/src/widgets/raw_editor/raw_editor.dart | 7 +- .../widgets/toolbar/buttons/clear_format.dart | 2 +- lib/src/widgets/toolbar/buttons/color.dart | 2 +- .../widgets/toolbar/buttons/font_family.dart | 27 +-- .../widgets/toolbar/buttons/font_size.dart | 28 +-- lib/src/widgets/toolbar/buttons/history.dart | 33 ++- lib/src/widgets/toolbar/buttons/indent.dart | 2 +- .../widgets/toolbar/buttons/link_style.dart | 193 ++++++++++++------ lib/src/widgets/toolbar/buttons/search.dart | 72 ------- .../toolbar/buttons/search/search.dart | 187 +++++++++++++++++ .../{ => buttons/search}/search_dialog.dart | 85 ++++++-- .../toolbar/buttons/select_alignment.dart | 31 +-- .../toolbar/buttons/select_header_style.dart | 151 ++++++++------ .../toolbar/buttons/toggle_check_list.dart | 29 +-- .../widgets/toolbar/buttons/toggle_style.dart | 18 +- lib/src/widgets/toolbar/enum.dart | 9 + lib/src/widgets/toolbar/toolbar.dart | 93 ++++----- pubspec.yaml | 2 +- 32 files changed, 791 insertions(+), 406 deletions(-) create mode 100644 lib/src/models/config/toolbar/buttons/link_style.dart create mode 100644 lib/src/models/config/toolbar/buttons/search.dart create mode 100644 lib/src/models/config/toolbar/buttons/select_header_style.dart delete mode 100644 lib/src/widgets/toolbar/buttons/search.dart create mode 100644 lib/src/widgets/toolbar/buttons/search/search.dart rename lib/src/widgets/toolbar/{ => buttons/search}/search_dialog.dart (68%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d1cf4b..7184615e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [7.9.0] +- Buttons Improvemenets +- Refactor all the button configurations that used in `QuillToolbar.basic()` but there are still few lefts +- **Breaking change**: Remove some configurations from the QuillToolbar and move them to the new `QuillProvider`, please notice this is a development version and this might be changed in the next few days, the stable release will be ready in less than 3 weeks +- Update `flutter_quill_extensions` and it will be published into pub.dev soon. +- Allow you to customize the search dialog by custom callback with child builder + ## [7.8.0] - **Important note**: this is not test release yet, it works but need more test and changes and breaking changes, we don't have development version and it will help us if you try the latest version and report the issues in Github but if you want a stable version please use `7.4.16`. this refactoring process will not take long and should be done less than three weeks with the testing. - We managed to refactor most of the buttons configurations and customizations in the `QuillProvider`, only three lefts then will start on refactoring the toolbar configurations diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 25e923df..5321e453 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -262,7 +262,7 @@ class _HomePageState extends State<HomePage> { webImagePickImpl: _webImagePickImpl, ), showAlignmentButtons: true, - afterButtonPressed: _focusNode.requestFocus, + // afterButtonPressed: _focusNode.requestFocus, ); } if (_isDesktop()) { @@ -272,7 +272,7 @@ class _HomePageState extends State<HomePage> { filePickImpl: openFileSystemPickerForDesktop, ), showAlignmentButtons: true, - afterButtonPressed: _focusNode.requestFocus, + // afterButtonPressed: _focusNode.requestFocus, ); } return QuillToolbar.basic( @@ -288,7 +288,7 @@ class _HomePageState extends State<HomePage> { // cameraPickSettingSelector: _selectCameraPickSetting, ), showAlignmentButtons: true, - afterButtonPressed: _focusNode.requestFocus, + // afterButtonPressed: _focusNode.requestFocus, ); } @@ -320,6 +320,12 @@ class _HomePageState extends State<HomePage> { return SafeArea( child: QuillProvider( configurations: QuillConfigurations( + toolbarConfigurations: QuillToolbarConfigurations( + buttonOptions: QuillToolbarButtonOptions( + base: QuillToolbarBaseButtonOptions( + afterButtonPressed: _focusNode.requestFocus, + ), + )), editorConfigurations: const QuillEditorConfigurations( placeholder: 'Add content', // ignore: avoid_redundant_argument_values diff --git a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart index 6fab2583..2c512567 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart @@ -53,7 +53,7 @@ class CameraButton extends StatelessWidget { final iconFillColor = iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - return QuillIconButton( + return QuillToolbarIconButton( icon: Icon(icon, size: iconSize, color: iconColor), tooltip: tooltip, highlightElevation: 0, diff --git a/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart index 5c7c5684..882d067c 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart @@ -34,7 +34,7 @@ class FormulaButton extends StatelessWidget { final iconFillColor = iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - return QuillIconButton( + return QuillToolbarIconButton( icon: Icon(icon, size: iconSize, color: iconColor), tooltip: tooltip, highlightElevation: 0, diff --git a/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart index 55420518..af5f924f 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart @@ -51,7 +51,7 @@ class ImageButton extends StatelessWidget { final iconFillColor = iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - return QuillIconButton( + return QuillToolbarIconButton( icon: Icon(icon, size: iconSize, color: iconColor), tooltip: tooltip, highlightElevation: 0, diff --git a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart index 8a13a5fc..a5eae3dd 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart @@ -94,7 +94,7 @@ class MediaButton extends StatelessWidget { final iconFillColor = iconTheme?.iconUnselectedFillColor ?? fillColor ?? theme.canvasColor; - return QuillIconButton( + return QuillToolbarIconButton( icon: Icon(icon, size: iconSize, color: iconColor), tooltip: tooltip, highlightElevation: 0, diff --git a/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart index 3f0a0226..f09d193b 100644 --- a/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart +++ b/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart @@ -53,7 +53,7 @@ class VideoButton extends StatelessWidget { final iconFillColor = iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - return QuillIconButton( + return QuillToolbarIconButton( icon: Icon(icon, size: iconSize, color: iconColor), tooltip: tooltip, highlightElevation: 0, diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index b0d616c7..e45814ad 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter: sdk: flutter - flutter_quill: ^7.5.0 + flutter_quill: ^7.8.0 # In case you are working on changes for both libraries, # flutter_quill: # path: ~/development/playground/framework_based/flutter/flutter-quill diff --git a/lib/src/models/config/shared_configurations.dart b/lib/src/models/config/shared_configurations.dart index acf3b435..ef6eb3a2 100644 --- a/lib/src/models/config/shared_configurations.dart +++ b/lib/src/models/config/shared_configurations.dart @@ -1,7 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart' show Color, Colors, Locale; + import './editor/configurations.dart' show QuillEditorConfigurations; import './toolbar/configurations.dart' show QuillToolbarConfigurations; +import '../themes/quill_dialog_theme.dart'; import 'others/animations.dart'; export './others/animations.dart'; @@ -11,6 +13,7 @@ export './others/animations.dart'; class QuillSharedConfigurations extends Equatable { const QuillSharedConfigurations({ this.dialogBarrierColor = Colors.black54, + this.dialogTheme, this.locale, this.animationConfigurations = const QuillAnimationConfigurations( checkBoxPointItem: false, @@ -22,6 +25,10 @@ class QuillSharedConfigurations extends Equatable { /// The barrier color of the shown dialogs final Color dialogBarrierColor; + /// The default dialog theme for all the dialogs for quill editor and + /// quill toolbar + final QuillDialogTheme? dialogTheme; + /// The locale to use for the editor and toolbar, defaults to system locale /// More https://github.com/singerdmx/flutter-quill#translation final Locale? locale; diff --git a/lib/src/models/config/toolbar/buttons/link_style.dart b/lib/src/models/config/toolbar/buttons/link_style.dart new file mode 100644 index 00000000..d7860b0d --- /dev/null +++ b/lib/src/models/config/toolbar/buttons/link_style.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart' show Color; + +import '../../../../widgets/toolbar/toolbar.dart'; +import '../../../structs/link_dialog_action.dart'; +import '../../../themes/quill_dialog_theme.dart'; + +class QuillToolbarLinkStyleButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarLinkStyleButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarLinkStyleButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarLinkStyleButtonOptions, + QuillToolbarLinkStyleButtonExtraOptions> { + const QuillToolbarLinkStyleButtonOptions({ + this.dialogTheme, + this.linkRegExp, + this.linkDialogAction, + this.dialogBarrierColor, + this.iconSize, + super.iconData, + super.globalIconSize, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final double? iconSize; + final QuillDialogTheme? dialogTheme; + final RegExp? linkRegExp; + final LinkDialogAction? linkDialogAction; + final Color? dialogBarrierColor; +} diff --git a/lib/src/models/config/toolbar/buttons/search.dart b/lib/src/models/config/toolbar/buttons/search.dart new file mode 100644 index 00000000..b4505408 --- /dev/null +++ b/lib/src/models/config/toolbar/buttons/search.dart @@ -0,0 +1,61 @@ +import 'package:flutter/widgets.dart' show Color; + +import '../../../../../flutter_quill.dart'; + +class QuillToolbarSearchButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarSearchButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarSearchButtonOptions extends QuillToolbarBaseButtonOptions { + const QuillToolbarSearchButtonOptions({ + super.iconData, + super.controller, + super.childBuilder, + super.tooltip, + super.afterButtonPressed, + super.iconTheme, + this.dialogTheme, + this.iconSize, + this.dialogBarrierColor, + this.fillColor, + this.customOnPressedCallback, + }); + + final QuillDialogTheme? dialogTheme; + final double? iconSize; + + /// By default will be [dialogBarrierColor] from [QuillSharedConfigurations] + final Color? dialogBarrierColor; + + final Color? fillColor; + + /// By default we will show simple search dialog ui + /// you can pass value to this callback to change this + final QuillToolbarSearchButtomOnPressedCallback? customOnPressedCallback; +} + +typedef QuillToolbarSearchButtomOnPressedCallback = Future<void> Function( + QuillController controller, +); + +// typedef QuillToolbarSearchButtonFindTextCallback = List<int> Function({ +// required int index, +// required String text, +// required QuillController controller, +// required List<int> offsets, +// required bool wholeWord, +// required bool caseSensitive, +// bool moveToPosition, +// }); + +// typedef QuillToolbarSearchButtonMoveToPositionCallback = void Function({ +// required int index, +// required String text, +// required QuillController controller, +// required List<int> offsets, +// }); diff --git a/lib/src/models/config/toolbar/buttons/select_header_style.dart b/lib/src/models/config/toolbar/buttons/select_header_style.dart new file mode 100644 index 00000000..4a742582 --- /dev/null +++ b/lib/src/models/config/toolbar/buttons/select_header_style.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart' show Axis; + +import '../../../../widgets/toolbar/toolbar.dart'; +import '../../../documents/attribute.dart'; + +class QuillToolbarSelectHeaderStyleButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarSelectHeaderStyleButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarSelectHeaderStyleButtonsOptions + extends QuillToolbarBaseButtonOptions< + QuillToolbarSelectHeaderStyleButtonsOptions, + QuillToolbarSelectHeaderStyleButtonExtraOptions> { + const QuillToolbarSelectHeaderStyleButtonsOptions({ + super.afterButtonPressed, + super.childBuilder, + super.controller, + super.iconData, + super.iconTheme, + super.tooltip, + this.axis, + this.attributes = const [ + Attribute.header, + Attribute.h1, + Attribute.h2, + Attribute.h3, + ], + this.iconSize, + }); + + final List<Attribute> attributes; + + /// By default we will the toolbar axis from [QuillToolbarConfigurations] + final Axis? axis; + final double? iconSize; +} diff --git a/lib/src/models/config/toolbar/configurations.dart b/lib/src/models/config/toolbar/configurations.dart index 7a0739ed..fcabae28 100644 --- a/lib/src/models/config/toolbar/configurations.dart +++ b/lib/src/models/config/toolbar/configurations.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart' show immutable; +import 'package:flutter/widgets.dart' show Axis; import 'buttons/base.dart'; import 'buttons/clear_format.dart'; @@ -8,17 +9,24 @@ import 'buttons/font_family.dart'; import 'buttons/font_size.dart'; import 'buttons/history.dart'; import 'buttons/indent.dart'; +import 'buttons/link_style.dart'; +import 'buttons/search.dart'; import 'buttons/select_alignment.dart'; +import 'buttons/select_header_style.dart'; import 'buttons/toggle_check_list.dart'; import 'buttons/toggle_style.dart'; +export './../../../widgets/toolbar/buttons/search/search_dialog.dart'; export './buttons/base.dart'; export './buttons/clear_format.dart'; export './buttons/color.dart'; export './buttons/font_family.dart'; export './buttons/font_size.dart'; export './buttons/history.dart'; +export './buttons/link_style.dart'; +export './buttons/search.dart'; export './buttons/select_alignment.dart'; +export './buttons/select_header_style.dart'; export './buttons/toggle_check_list.dart'; export './buttons/toggle_style.dart'; @@ -42,8 +50,10 @@ class QuillToolbarConfigurations extends Equatable { this.multiRowsDisplay = true, this.fontFamilyValues, this.fontSizesValues, + this.axis = Axis.horizontal, - /// By default it will calculated based on the [baseOptions] iconSize + /// By default it will calculated based on the [globalIconSize] from + /// [base] in [QuillToolbarButtonOptions] /// You can change it but the the change only apply if /// the [multiRowsDisplay] is false, if [multiRowsDisplay] then the value /// will be [kDefaultIconSize] * 2 @@ -93,6 +103,11 @@ class QuillToolbarConfigurations extends Equatable { /// ``` final Map<String, String>? fontSizesValues; + /// Toolbar axis + /// it will apply only for [QuillToolbar.basic] + /// we will update that logic soon + final Axis axis; + @override List<Object?> get props => [ buttonOptions, @@ -100,6 +115,7 @@ class QuillToolbarConfigurations extends Equatable { fontFamilyValues, fontSizesValues, toolbarSize, + axis, ]; } @@ -137,6 +153,10 @@ class QuillToolbarButtonOptions extends Equatable { this.clearFormat = const QuillToolbarClearFormatButtonOptions(), this.selectAlignmentButtons = const QuillToolbarSelectAlignmentButtonOptions(), + this.search = const QuillToolbarSearchButtonOptions(), + this.selectHeaderStyleButtons = + const QuillToolbarSelectHeaderStyleButtonsOptions(), + this.linkStyle = const QuillToolbarLinkStyleButtonOptions(), }); /// The base configurations for all the buttons which will apply to all @@ -173,6 +193,15 @@ class QuillToolbarButtonOptions extends Equatable { /// and you have child builder final QuillToolbarSelectAlignmentButtonOptions selectAlignmentButtons; + final QuillToolbarSearchButtonOptions search; + + /// The reason we call this buttons in the end because this is responsible + /// for all the header style buttons and not just one, you still + /// can customize it and you also have child builder + final QuillToolbarSelectHeaderStyleButtonsOptions selectHeaderStyleButtons; + + final QuillToolbarLinkStyleButtonOptions linkStyle; + @override List<Object?> get props => [ base, diff --git a/lib/src/models/themes/quill_custom_button.dart b/lib/src/models/themes/quill_custom_button.dart index 6f5b5365..7caee256 100644 --- a/lib/src/models/themes/quill_custom_button.dart +++ b/lib/src/models/themes/quill_custom_button.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; -class QuillCustomButton { +import '../../widgets/toolbar/toolbar.dart'; + +class QuillCustomButton extends QuillToolbarBaseButtonOptions { const QuillCustomButton({ - this.icon, + super.iconData, this.iconColor, this.onTap, - this.tooltip, + super.tooltip, + this.iconSize, this.child, + super.iconTheme, }); - ///The icon widget - final IconData? icon; - ///The icon color; final Color? iconColor; @@ -21,6 +22,5 @@ class QuillCustomButton { ///The customButton placeholder final Widget? child; - /// The button tooltip. - final String? tooltip; + final double? iconSize; } diff --git a/lib/src/widgets/raw_editor/raw_editor.dart b/lib/src/widgets/raw_editor/raw_editor.dart index c71bbd08..613e72b0 100644 --- a/lib/src/widgets/raw_editor/raw_editor.dart +++ b/lib/src/widgets/raw_editor/raw_editor.dart @@ -50,7 +50,7 @@ import '../text_block.dart'; import '../text_line.dart'; import '../text_selection.dart'; import '../toolbar/buttons/link_style2.dart'; -import '../toolbar/search_dialog.dart'; +import '../toolbar/buttons/search/search_dialog.dart'; import 'raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor_state_text_input_client_mixin.dart'; @@ -2628,7 +2628,10 @@ class _OpenSearchAction extends ContextAction<OpenSearchIntent> { } await showDialog<String>( context: context, - builder: (_) => SearchDialog(controller: state.controller, text: ''), + builder: (_) => QuillToolbarSearchDialog( + controller: state.controller, + text: '', + ), ); } diff --git a/lib/src/widgets/toolbar/buttons/clear_format.dart b/lib/src/widgets/toolbar/buttons/clear_format.dart index f62a7354..97a7994f 100644 --- a/lib/src/widgets/toolbar/buttons/clear_format.dart +++ b/lib/src/widgets/toolbar/buttons/clear_format.dart @@ -18,7 +18,7 @@ class QuillToolbarClearFormatButton extends StatelessWidget { final QuillToolbarClearFormatButtonOptions options; QuillController get controller { - return options.controller ?? _controller; + return _controller; } double _iconSize(BuildContext context) { diff --git a/lib/src/widgets/toolbar/buttons/color.dart b/lib/src/widgets/toolbar/buttons/color.dart index a9480edd..69671c13 100644 --- a/lib/src/widgets/toolbar/buttons/color.dart +++ b/lib/src/widgets/toolbar/buttons/color.dart @@ -100,7 +100,7 @@ class _QuillToolbarColorButtonState extends State<QuillToolbarColorButton> { } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { diff --git a/lib/src/widgets/toolbar/buttons/font_family.dart b/lib/src/widgets/toolbar/buttons/font_family.dart index 5ec16f6d..a82424a1 100644 --- a/lib/src/widgets/toolbar/buttons/font_family.dart +++ b/lib/src/widgets/toolbar/buttons/font_family.dart @@ -38,12 +38,6 @@ class _QuillToolbarFontFamilyButtonState return widget.options; } - /// Since it's not safe to call anything related to the context in dispose - /// then we will save a reference to the [controller] - /// and update it in [didChangeDependencies] - /// and use it in dispose method - late QuillController _controller; - Style get _selectionStyle => controller.getSelectionStyle(); @override @@ -52,27 +46,14 @@ class _QuillToolbarFontFamilyButtonState _initState(); } - Future<void> _initState() async { - if (isFlutterTest()) { - // We don't need to listen for changes in the tests - return; - } - await Future.delayed(Duration.zero); - setState(() { - _currentValue = _defaultDisplayText; - }); + void _initState() { + _currentValue = _defaultDisplayText; controller.addListener(_didChangeEditingValue); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _controller = controller; - } - @override void dispose() { - _controller.removeListener(_didChangeEditingValue); + controller.removeListener(_didChangeEditingValue); super.dispose(); } @@ -128,7 +109,7 @@ class _QuillToolbarFontFamilyButtonState } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { diff --git a/lib/src/widgets/toolbar/buttons/font_size.dart b/lib/src/widgets/toolbar/buttons/font_size.dart index d4622d55..5eebe463 100644 --- a/lib/src/widgets/toolbar/buttons/font_size.dart +++ b/lib/src/widgets/toolbar/buttons/font_size.dart @@ -34,12 +34,6 @@ class _QuillToolbarFontSizeButtonState return widget.options; } - /// Since it's not safe to call anything related to the context in dispose - /// then we will save a reference to the [controller] - /// and update it in [didChangeDependencies] - /// and use it in dispose method - late QuillController _controller; - Map<String, String> get rawItemsMap { final fontSizes = options.rawItemsMap ?? context.requireQuillToolbarConfigurations.fontSizesValues ?? @@ -65,33 +59,21 @@ class _QuillToolbarFontSizeButtonState _initState(); } - Future<void> _initState() async { - if (isFlutterTest()) { - return; - } - await Future.delayed(Duration.zero); - setState(() { - _currentValue = _defaultDisplayText; - }); + void _initState() { + _currentValue = _defaultDisplayText; controller.addListener(_didChangeEditingValue); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _controller = controller; - } - @override void dispose() { - _controller.removeListener(_didChangeEditingValue); + controller.removeListener(_didChangeEditingValue); super.dispose(); } @override void didUpdateWidget(covariant QuillToolbarFontSizeButton oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.controller == controller) { + if (controller == oldWidget.controller) { return; } controller @@ -119,7 +101,7 @@ class _QuillToolbarFontSizeButtonState } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { diff --git a/lib/src/widgets/toolbar/buttons/history.dart b/lib/src/widgets/toolbar/buttons/history.dart index 817d9410..f4bb102b 100644 --- a/lib/src/widgets/toolbar/buttons/history.dart +++ b/lib/src/widgets/toolbar/buttons/history.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; -import '../../../../extensions.dart'; import '../../../../translations.dart'; import '../../../utils/extensions/build_context.dart'; -import '../../../utils/extensions/quill_controller.dart'; import '../../controller.dart'; import '../toolbar.dart'; class QuillToolbarHistoryButton extends StatefulWidget { const QuillToolbarHistoryButton({ required this.options, + required this.controller, super.key, }); final QuillToolbarHistoryButtonOptions options; + final QuillController controller; @override _QuillToolbarHistoryButtonState createState() => @@ -29,7 +29,7 @@ class _QuillToolbarHistoryButtonState extends State<QuillToolbarHistoryButton> { } QuillController get controller { - return options.controller.notNull(context); + return widget.controller; } @override @@ -38,17 +38,12 @@ class _QuillToolbarHistoryButtonState extends State<QuillToolbarHistoryButton> { _listenForChanges(); // Listen for changes and change it } - Future<void> _listenForChanges() async { - if (isFlutterTest()) { - // We don't need to listen for changes in the tests - return; - } - await Future.delayed(Duration.zero); // Wait for the widget to built + void _listenForChanges() { _updateCanPressed(); // Set the init state // Listen for changes and change it controller.changes.listen((event) async { - _updateCanPressed(); + _updateCanPressedWithSetState(); }); } @@ -116,16 +111,18 @@ class _QuillToolbarHistoryButtonState extends State<QuillToolbarHistoryButton> { ); } - void _updateCanPressed() { + void _updateCanPressedWithSetState() { if (!mounted) return; - setState(() { - if (options.isUndo) { - _canPressed = controller.hasUndo; - return; - } - _canPressed = controller.hasRedo; - }); + setState(_updateCanPressed); + } + + void _updateCanPressed() { + if (options.isUndo) { + _canPressed = controller.hasUndo; + return; + } + _canPressed = controller.hasRedo; } void _updateHistory() { diff --git a/lib/src/widgets/toolbar/buttons/indent.dart b/lib/src/widgets/toolbar/buttons/indent.dart index f2fa171f..c22c2c1f 100644 --- a/lib/src/widgets/toolbar/buttons/indent.dart +++ b/lib/src/widgets/toolbar/buttons/indent.dart @@ -30,7 +30,7 @@ class _QuillToolbarIndentButtonState extends State<QuillToolbarIndentButton> { } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { diff --git a/lib/src/widgets/toolbar/buttons/link_style.dart b/lib/src/widgets/toolbar/buttons/link_style.dart index 277f20d3..efeadf73 100644 --- a/lib/src/widgets/toolbar/buttons/link_style.dart +++ b/lib/src/widgets/toolbar/buttons/link_style.dart @@ -6,6 +6,7 @@ import '../../../models/structs/link_dialog_action.dart'; import '../../../models/themes/quill_dialog_theme.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../translations/toolbar.i18n.dart'; +import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../../link.dart'; import '../toolbar.dart'; @@ -13,28 +14,21 @@ import '../toolbar.dart'; class QuillToolbarLinkStyleButton extends StatefulWidget { const QuillToolbarLinkStyleButton({ required this.controller, - this.iconSize = kDefaultIconSize, - this.icon, - this.iconTheme, - this.dialogTheme, - this.afterButtonPressed, - this.tooltip, - this.linkRegExp, - this.linkDialogAction, - this.dialogBarrierColor = Colors.black54, - Key? key, - }) : super(key: key); + required this.options, + super.key, + }); final QuillController controller; - final IconData? icon; - final double iconSize; - final QuillIconTheme? iconTheme; - final QuillDialogTheme? dialogTheme; - final VoidCallback? afterButtonPressed; - final String? tooltip; - final RegExp? linkRegExp; - final LinkDialogAction? linkDialogAction; - final Color dialogBarrierColor; + // final IconData? icon; + // final double iconSize; + // final QuillIconTheme? iconTheme; + // final QuillDialogTheme? dialogTheme; + // final VoidCallback? afterButtonPressed; + // final String? tooltip; + // final RegExp? linkRegExp; + // final LinkDialogAction? linkDialogAction; + // final Color dialogBarrierColor; + final QuillToolbarLinkStyleButtonOptions options; @override _QuillToolbarLinkStyleButtonState createState() => @@ -50,22 +44,68 @@ class _QuillToolbarLinkStyleButtonState @override void initState() { super.initState(); - widget.controller.addListener(_didChangeSelection); + controller.addListener(_didChangeSelection); } @override void didUpdateWidget(covariant QuillToolbarLinkStyleButton oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { + if (oldWidget.controller != controller) { oldWidget.controller.removeListener(_didChangeSelection); - widget.controller.addListener(_didChangeSelection); + controller.addListener(_didChangeSelection); } } @override void dispose() { super.dispose(); - widget.controller.removeListener(_didChangeSelection); + controller.removeListener(_didChangeSelection); + } + + QuillController get controller { + return widget.controller; + } + + QuillToolbarLinkStyleButtonOptions get options { + return widget.options; + } + + double get iconSize { + final baseFontSize = baseButtonExtraOptions.globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? get afterButtonPressed { + return options.afterButtonPressed ?? + baseButtonExtraOptions.afterButtonPressed; + } + + QuillIconTheme? get iconTheme { + return options.iconTheme ?? baseButtonExtraOptions.iconTheme; + } + + QuillToolbarBaseButtonOptions get baseButtonExtraOptions { + return context.requireQuillToolbarBaseButtonOptions; + } + + String get tooltip { + return options.tooltip ?? + baseButtonExtraOptions.tooltip ?? + 'Insert URL'.i18n; + } + + IconData get iconData { + return options.iconData ?? baseButtonExtraOptions.iconData ?? Icons.link; + } + + Color get dialogBarrierColor { + return options.dialogBarrierColor ?? + context.requireQuillSharedConfigurations.dialogBarrierColor; + } + + RegExp get linkRegExp { + return options.linkRegExp ?? RegExp(r'https?://\S+'); } @override @@ -73,87 +113,113 @@ class _QuillToolbarLinkStyleButtonState final theme = Theme.of(context); final isToggled = _getLinkAttributeValue() != null; final pressedHandler = () => _openLinkDialog(context); + + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions.childBuilder; + if (childBuilder != null) { + return childBuilder( + QuillToolbarLinkStyleButtonOptions( + afterButtonPressed: afterButtonPressed, + controller: controller, + dialogBarrierColor: dialogBarrierColor, + dialogTheme: options.dialogTheme, + iconData: iconData, + iconSize: iconSize, + tooltip: tooltip, + linkDialogAction: options.linkDialogAction, + linkRegExp: linkRegExp, + iconTheme: iconTheme, + ), + QuillToolbarLinkStyleButtonExtraOptions( + context: context, + controller: controller, + onPressed: () { + pressedHandler(); + afterButtonPressed?.call(); + }, + ), + ); + } return QuillToolbarIconButton( - tooltip: widget.tooltip, + tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, - size: widget.iconSize * kIconButtonFactor, + size: iconSize * kIconButtonFactor, icon: Icon( - widget.icon ?? Icons.link, - size: widget.iconSize, + iconData, + size: iconSize, color: isToggled - ? (widget.iconTheme?.iconSelectedColor ?? - theme.primaryIconTheme.color) - : (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), + ? (iconTheme?.iconSelectedColor ?? theme.primaryIconTheme.color) + : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), ), fillColor: isToggled - ? (widget.iconTheme?.iconSelectedFillColor ?? - Theme.of(context).primaryColor) - : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), - borderRadius: widget.iconTheme?.borderRadius ?? 2, + ? (iconTheme?.iconSelectedFillColor ?? Theme.of(context).primaryColor) + : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), + borderRadius: iconTheme?.borderRadius ?? 2, onPressed: pressedHandler, - afterPressed: widget.afterButtonPressed, + afterPressed: afterButtonPressed, ); } - void _openLinkDialog(BuildContext context) { - showDialog<_TextLink>( + Future<void> _openLinkDialog(BuildContext context) async { + // TODO: Add a custom call back to customize this just like in the search + // button + final value = await showDialog<_TextLink>( context: context, - barrierColor: widget.dialogBarrierColor, + barrierColor: dialogBarrierColor, builder: (ctx) { final link = _getLinkAttributeValue(); - final index = widget.controller.selection.start; + final index = controller.selection.start; var text; if (link != null) { // text should be the link's corresponding text, not selection - final leaf = - widget.controller.document.querySegmentLeafNode(index).leaf; + final leaf = controller.document.querySegmentLeafNode(index).leaf; if (leaf != null) { text = leaf.toPlainText(); } } - final len = widget.controller.selection.end - index; - text ??= - len == 0 ? '' : widget.controller.document.getPlainText(index, len); + final len = controller.selection.end - index; + text ??= len == 0 ? '' : controller.document.getPlainText(index, len); return _LinkDialog( - dialogTheme: widget.dialogTheme, + dialogTheme: options.dialogTheme, link: link, text: text, - linkRegExp: widget.linkRegExp, - action: widget.linkDialogAction, + linkRegExp: linkRegExp, + action: options.linkDialogAction, ); }, - ).then( - (value) { - if (value != null) _linkSubmitted(value); - }, ); + if (value == null) { + return; + } + _linkSubmitted(value); } String? _getLinkAttributeValue() { - return widget.controller - .getSelectionStyle() - .attributes[Attribute.link.key] - ?.value; + return controller.getSelectionStyle().attributes[Attribute.link.key]?.value; } void _linkSubmitted(_TextLink value) { - var index = widget.controller.selection.start; - var length = widget.controller.selection.end - index; + var index = controller.selection.start; + var length = controller.selection.end - index; if (_getLinkAttributeValue() != null) { // text should be the link's corresponding text, not selection - final leaf = widget.controller.document.querySegmentLeafNode(index).leaf; + final leaf = controller.document.querySegmentLeafNode(index).leaf; if (leaf != null) { final range = getLinkRange(leaf); index = range.start; length = range.end - range.start; } } - widget.controller.replaceText(index, length, value.text, null); - widget.controller - .formatText(index, value.text.length, LinkAttribute(value.link)); + controller + ..replaceText(index, length, value.text, null) + ..formatText( + index, + value.text.length, + LinkAttribute(value.link), + ); } } @@ -164,8 +230,7 @@ class _LinkDialog extends StatefulWidget { this.text, this.linkRegExp, this.action, - Key? key, - }) : super(key: key); + }); final QuillDialogTheme? dialogTheme; final String? link; diff --git a/lib/src/widgets/toolbar/buttons/search.dart b/lib/src/widgets/toolbar/buttons/search.dart deleted file mode 100644 index 3e8fa792..00000000 --- a/lib/src/widgets/toolbar/buttons/search.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../models/themes/quill_dialog_theme.dart'; -import '../../../models/themes/quill_icon_theme.dart'; -import '../../controller.dart'; -import '../search_dialog.dart'; -import '../toolbar.dart'; - -class QuillToolbarSearchButton extends StatelessWidget { - const QuillToolbarSearchButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.fillColor, - this.iconTheme, - this.dialogBarrierColor = Colors.black54, - this.dialogTheme, - this.afterButtonPressed, - this.tooltip, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final QuillController controller; - final Color? fillColor; - final Color dialogBarrierColor; - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - final VoidCallback? afterButtonPressed; - final String? tooltip; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - - return QuillToolbarIconButton( - tooltip: tooltip, - icon: Icon(icon, size: iconSize, color: iconColor), - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * kIconButtonFactor, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _onPressedHandler(context), - afterPressed: afterButtonPressed, - ); - } - - Future<void> _onPressedHandler(BuildContext context) async { - final value = await showDialog<String>( - barrierColor: dialogBarrierColor, - context: context, - builder: (_) => SearchDialog( - controller: controller, - dialogTheme: dialogTheme, - text: '', - ), - ); - _searchSubmitted(value); - } - - void _searchSubmitted(String? value) { - // If we are doing nothing here then why we care about the result?? - } -} diff --git a/lib/src/widgets/toolbar/buttons/search/search.dart b/lib/src/widgets/toolbar/buttons/search/search.dart new file mode 100644 index 00000000..3ff46a9f --- /dev/null +++ b/lib/src/widgets/toolbar/buttons/search/search.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; + +import '../../../../../translations.dart'; +import '../../../../models/themes/quill_dialog_theme.dart'; +import '../../../../models/themes/quill_icon_theme.dart'; +import '../../../../utils/extensions/build_context.dart'; +import '../../../controller.dart'; +import '../../toolbar.dart'; + +class QuillToolbarSearchButton extends StatelessWidget { + const QuillToolbarSearchButton({ + required QuillController controller, + required this.options, + super.key, + }) : _controller = controller; + + final QuillController _controller; + final QuillToolbarSearchButtonOptions options; + + QuillController get controller { + return _controller; + } + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.search; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + ('Search'.i18n); + } + + Color _dialogBarrierColor(BuildContext context) { + return options.dialogBarrierColor ?? + context.requireQuillSharedConfigurations.dialogBarrierColor; + } + + QuillDialogTheme? _dialogTheme(BuildContext context) { + return options.dialogTheme ?? + context.requireQuillSharedConfigurations.dialogTheme; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + final tooltip = _tooltip(context); + final iconData = _iconData(context); + final iconSize = _iconSize(context); + final afterButtonPressed = _afterButtonPressed(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + if (childBuilder != null) { + return childBuilder( + QuillToolbarSearchButtonOptions( + afterButtonPressed: afterButtonPressed, + controller: controller, + dialogBarrierColor: _dialogBarrierColor(context), + dialogTheme: _dialogTheme(context), + fillColor: options.fillColor, + iconData: _iconData(context), + iconSize: _iconSize(context), + tooltip: _tooltip(context), + iconTheme: _iconTheme(context), + ), + QuillToolbarSearchButtonExtraOptions( + controller: controller, + context: context, + onPressed: () { + _sharedOnPressed(context); + afterButtonPressed?.call(); + }, + ), + ); + } + + return QuillToolbarIconButton( + tooltip: tooltip, + icon: Icon( + iconData, + size: iconSize, + color: iconColor, + ), + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * kIconButtonFactor, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + afterPressed: afterButtonPressed, + ); + } + + Future<void> _sharedOnPressed(BuildContext context) async { + final customCallback = options.customOnPressedCallback; + if (customCallback != null) { + await customCallback( + controller, + ); + return; + } + await showDialog<String>( + barrierColor: _dialogBarrierColor(context), + context: context, + builder: (_) => QuillToolbarSearchDialog( + controller: controller, + dialogTheme: _dialogTheme(context), + text: '', + ), + ); + } + + // Those functions ((findText, moveToPosition)) are not ready yet. + // but consider moving them to a better place + // List<int> _findText({ + // required int index, + // required String text, + // required QuillController controller, + // required List<int> offsets, + // required bool wholeWord, + // required bool caseSensitive, + // bool moveToPosition = true, + // }) { + // if (text.isEmpty) { + // return List.empty(); + // } + // final newOffsets = controller.document.search( + // text, + // caseSensitive: caseSensitive, + // wholeWord: wholeWord, + // ); + // index = 0; // TODO: This might need to be updated... + // if (offsets.isNotEmpty && moveToPosition) { + // _moveToPosition( + // index: index, + // text: text, + // controller: controller, + // offsets: offsets, + // ); + // } + // return newOffsets; + // } + + // void _moveToPosition({ + // required int index, + // required String text, + // required QuillController controller, + // required List<int> offsets, + // }) { + // controller.updateSelection( + // TextSelection( + // baseOffset: offsets[index], + // extentOffset: offsets[index] + text.length, + // ), + // ChangeSource.LOCAL, + // ); + // } +} diff --git a/lib/src/widgets/toolbar/search_dialog.dart b/lib/src/widgets/toolbar/buttons/search/search_dialog.dart similarity index 68% rename from lib/src/widgets/toolbar/search_dialog.dart rename to lib/src/widgets/toolbar/buttons/search/search_dialog.dart index 136ec195..dc279ab4 100644 --- a/lib/src/widgets/toolbar/search_dialog.dart +++ b/lib/src/widgets/toolbar/buttons/search/search_dialog.dart @@ -1,27 +1,62 @@ import 'package:flutter/material.dart'; -import '../../../translations.dart'; -import '../../models/documents/document.dart'; -import '../../models/themes/quill_dialog_theme.dart'; -import '../controller.dart'; +import '../../../../../translations.dart'; +import '../../../../models/documents/document.dart'; +import '../../../../models/themes/quill_dialog_theme.dart'; +import '../../../controller.dart'; -class SearchDialog extends StatefulWidget { - const SearchDialog({ +@immutable +class QuillToolbarSearchDialogChildBuilderExtraOptions { + const QuillToolbarSearchDialogChildBuilderExtraOptions({ + required this.onFindTextPressed, + required this.moveToNext, + required this.moveToPrevious, + required this.onTextChanged, + required this.onEditingComplete, + required this.text, + required this.textEditingController, + required this.offsets, + required this.index, + required this.caseSensitive, + required this.wholeWord, + }); + final VoidCallback? onFindTextPressed; + final VoidCallback moveToNext; + final VoidCallback moveToPrevious; + final ValueChanged<String>? onTextChanged; + final VoidCallback? onEditingComplete; + final String text; + final TextEditingController textEditingController; + final List<int>? offsets; + final int index; + final bool caseSensitive; + final bool wholeWord; +} + +typedef QuillToolbarSearchDialogChildBuilder = Widget Function( + QuillToolbarSearchDialogChildBuilderExtraOptions extraOptions, +); + +class QuillToolbarSearchDialog extends StatefulWidget { + const QuillToolbarSearchDialog({ required this.controller, this.dialogTheme, this.text, - Key? key, - }) : super(key: key); + this.childBuilder, + super.key, + }); final QuillController controller; final QuillDialogTheme? dialogTheme; final String? text; + final QuillToolbarSearchDialogChildBuilder? childBuilder; @override - _SearchDialogState createState() => _SearchDialogState(); + _QuillToolbarSearchDialogState createState() => + _QuillToolbarSearchDialogState(); } -class _SearchDialogState extends State<SearchDialog> { +class _QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> { late String _text; late TextEditingController _controller; late List<int>? _offsets; @@ -55,6 +90,25 @@ class _SearchDialogState extends State<SearchDialog> { } } + final childBuilder = widget.childBuilder; + if (childBuilder != null) { + return childBuilder( + QuillToolbarSearchDialogChildBuilderExtraOptions( + onFindTextPressed: _findText, + onEditingComplete: _findText, + onTextChanged: _textChanged, + caseSensitive: _caseSensitive, + textEditingController: _controller, + index: _index, + offsets: _offsets, + text: _text, + wholeWord: _wholeWord, + moveToNext: _moveToNext, + moveToPrevious: _moveToPosition, + ), + ); + } + return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), @@ -140,6 +194,7 @@ class _SearchDialogState extends State<SearchDialog> { } void _findText() { + _text = _controller.text; if (_text.isEmpty) { return; } @@ -158,10 +213,12 @@ class _SearchDialogState extends State<SearchDialog> { void _moveToPosition() { widget.controller.updateSelection( - TextSelection( - baseOffset: _offsets![_index], - extentOffset: _offsets![_index] + _text.length), - ChangeSource.LOCAL); + TextSelection( + baseOffset: _offsets![_index], + extentOffset: _offsets![_index] + _text.length, + ), + ChangeSource.LOCAL, + ); } void _moveToPrevious() { diff --git a/lib/src/widgets/toolbar/buttons/select_alignment.dart b/lib/src/widgets/toolbar/buttons/select_alignment.dart index f56b4d68..4a5fe0d0 100644 --- a/lib/src/widgets/toolbar/buttons/select_alignment.dart +++ b/lib/src/widgets/toolbar/buttons/select_alignment.dart @@ -40,7 +40,7 @@ class _QuillToolbarSelectAlignmentButtonState extends State<QuillToolbarSelectAlignmentButton> { Attribute? _value; - Style get _selectionStyle => widget.controller.getSelectionStyle(); + Style get _selectionStyle => controller.getSelectionStyle(); @override void initState() { @@ -49,7 +49,7 @@ class _QuillToolbarSelectAlignmentButtonState _value = _selectionStyle.attributes[Attribute.align.key] ?? Attribute.leftAlignment; }); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); } QuillToolbarSelectAlignmentButtonOptions get options { @@ -57,7 +57,7 @@ class _QuillToolbarSelectAlignmentButtonState } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { @@ -123,12 +123,6 @@ class _QuillToolbarSelectAlignmentButtonState ); } - /// Since it's not safe to call anything related to the context in dispose - /// then we will save a reference to the [controller] - /// and update it in [didChangeDependencies] - /// and use it in dispose method - late QuillController _controller; - void _didChangeEditingValue() { setState(() { _value = _selectionStyle.attributes[Attribute.align.key] ?? @@ -139,23 +133,17 @@ class _QuillToolbarSelectAlignmentButtonState @override void didUpdateWidget(covariant QuillToolbarSelectAlignmentButton oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { + if (oldWidget.controller != controller) { oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); _value = _selectionStyle.attributes[Attribute.align.key] ?? Attribute.leftAlignment; } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _controller = controller; - } - @override void dispose() { - _controller.removeListener(_didChangeEditingValue); + controller.removeListener(_didChangeEditingValue); super.dispose(); } @@ -207,7 +195,7 @@ class _QuillToolbarSelectAlignmentButtonState if (childBuilder != null) { throw UnsupportedError( - 'Sorry but the `childBuilder` for the Select alignment button' + 'Sorry but the `childBuilder` for the Select alignment buttons' ' is not supported. Yet but we will work on that soon.', ); } @@ -245,11 +233,10 @@ class _QuillToolbarSelectAlignmentButtonState : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), onPressed: () { _valueAttribute[index] == Attribute.leftAlignment - ? widget.controller.formatSelection( + ? controller.formatSelection( Attribute.clone(Attribute.align, null), ) - : widget.controller - .formatSelection(_valueAttribute[index]); + : controller.formatSelection(_valueAttribute[index]); afterButtonPressed?.call(); }, child: Icon( diff --git a/lib/src/widgets/toolbar/buttons/select_header_style.dart b/lib/src/widgets/toolbar/buttons/select_header_style.dart index 0803e99e..5ce9270b 100644 --- a/lib/src/widgets/toolbar/buttons/select_header_style.dart +++ b/lib/src/widgets/toolbar/buttons/select_header_style.dart @@ -1,48 +1,34 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../../../extensions.dart'; +import '../../../../translations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../utils/widgets.dart'; +import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../toolbar.dart'; -class QuillToolbarSelectHeaderStyleButton extends StatefulWidget { - const QuillToolbarSelectHeaderStyleButton({ +class QuillToolbarSelectHeaderStyleButtons extends StatefulWidget { + const QuillToolbarSelectHeaderStyleButtons({ required this.controller, - this.axis = Axis.horizontal, - this.iconSize = kDefaultIconSize, - this.iconTheme, - this.attributes = const [ - Attribute.header, - Attribute.h1, - Attribute.h2, - Attribute.h3, - ], - this.afterButtonPressed, - this.tooltip, - Key? key, - }) : super(key: key); + required this.options, + super.key, + }); final QuillController controller; - final Axis axis; - final double iconSize; - final QuillIconTheme? iconTheme; - final List<Attribute> attributes; - final VoidCallback? afterButtonPressed; - final String? tooltip; + final QuillToolbarSelectHeaderStyleButtonsOptions options; @override - _QuillToolbarSelectHeaderStyleButtonState createState() => - _QuillToolbarSelectHeaderStyleButtonState(); + _QuillToolbarSelectHeaderStyleButtonsState createState() => + _QuillToolbarSelectHeaderStyleButtonsState(); } -class _QuillToolbarSelectHeaderStyleButtonState - extends State<QuillToolbarSelectHeaderStyleButton> { +class _QuillToolbarSelectHeaderStyleButtonsState + extends State<QuillToolbarSelectHeaderStyleButtons> { Attribute? _selectedAttribute; - Style get _selectionStyle => widget.controller.getSelectionStyle(); + Style get _selectionStyle => controller.getSelectionStyle(); final _valueToText = <Attribute, String>{ Attribute.header: 'N', @@ -57,61 +43,110 @@ class _QuillToolbarSelectHeaderStyleButtonState setState(() { _selectedAttribute = _getHeaderValue(); }); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); + } + + QuillToolbarSelectHeaderStyleButtonsOptions get options { + return widget.options; + } + + QuillController get controller { + return widget.controller; + } + + double get iconSize { + final baseFontSize = baseButtonExtraOptions.globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? get afterButtonPressed { + return options.afterButtonPressed ?? + baseButtonExtraOptions.afterButtonPressed; + } + + QuillIconTheme? get iconTheme { + return options.iconTheme ?? baseButtonExtraOptions.iconTheme; + } + + QuillToolbarBaseButtonOptions get baseButtonExtraOptions { + return context.requireQuillToolbarBaseButtonOptions; + } + + String get tooltip { + return options.tooltip ?? + baseButtonExtraOptions.tooltip ?? + 'Header style'.i18n; + } + + Axis get axis { + return options.axis ?? context.requireQuillToolbarConfigurations.axis; + } + + void _sharedOnPressed(Attribute attribute) { + final _attribute = + _selectedAttribute == attribute ? Attribute.header : attribute; + controller.formatSelection(_attribute); + afterButtonPressed?.call(); } @override Widget build(BuildContext context) { assert( - widget.attributes.every((element) => _valueToText.keys.contains(element)), + options.attributes.every( + (element) => _valueToText.keys.contains(element), + ), 'All attributes must be one of them: header, h1, h2 or h3', ); final theme = Theme.of(context); final style = TextStyle( fontWeight: FontWeight.w600, - fontSize: widget.iconSize * 0.7, + fontSize: iconSize * 0.7, ); - final children = widget.attributes.map((attribute) { + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions.childBuilder; + + if (childBuilder != null) { + throw UnsupportedError( + 'Sorry but the `childBuilder` for the Select header button' + ' is not supported. Yet but we will work on that soon.', + ); + } + + final children = options.attributes.map((attribute) { final isSelected = _selectedAttribute == attribute; return Padding( - // ignore: prefer_const_constructors - padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), + // Do we really need to ignore (prefer_const_constructors)?? + padding: EdgeInsets.symmetric(horizontal: !isWeb() ? 1.0 : 5.0), child: ConstrainedBox( constraints: BoxConstraints.tightFor( - width: widget.iconSize * kIconButtonFactor, - height: widget.iconSize * kIconButtonFactor, + width: iconSize * kIconButtonFactor, + height: iconSize * kIconButtonFactor, ), child: UtilityWidgets.maybeTooltip( - message: widget.tooltip, + message: tooltip, child: RawMaterialButton( hoverElevation: 0, highlightElevation: 0, elevation: 0, visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.iconTheme?.borderRadius ?? 2)), + borderRadius: + BorderRadius.circular(iconTheme?.borderRadius ?? 2)), fillColor: isSelected - ? (widget.iconTheme?.iconSelectedFillColor ?? + ? (iconTheme?.iconSelectedFillColor ?? Theme.of(context).primaryColor) - : (widget.iconTheme?.iconUnselectedFillColor ?? - theme.canvasColor), - onPressed: () { - final _attribute = _selectedAttribute == attribute - ? Attribute.header - : attribute; - widget.controller.formatSelection(_attribute); - widget.afterButtonPressed?.call(); - }, + : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), + onPressed: () => _sharedOnPressed(attribute), child: Text( _valueToText[attribute] ?? '', style: style.copyWith( color: isSelected - ? (widget.iconTheme?.iconSelectedColor ?? + ? (iconTheme?.iconSelectedColor ?? theme.primaryIconTheme.color) - : (widget.iconTheme?.iconUnselectedColor ?? + : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), ), ), @@ -121,7 +156,7 @@ class _QuillToolbarSelectHeaderStyleButtonState ); }).toList(); - return widget.axis == Axis.horizontal + return axis == Axis.horizontal ? Row( mainAxisSize: MainAxisSize.min, children: children, @@ -139,10 +174,10 @@ class _QuillToolbarSelectHeaderStyleButtonState } Attribute<dynamic> _getHeaderValue() { - final attr = widget.controller.toolbarButtonToggler[Attribute.header.key]; + final attr = controller.toolbarButtonToggler[Attribute.header.key]; if (attr != null) { // checkbox tapping causes controller.selection to go to offset 0 - widget.controller.toolbarButtonToggler.remove(Attribute.header.key); + controller.toolbarButtonToggler.remove(Attribute.header.key); return attr; } return _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; @@ -150,18 +185,18 @@ class _QuillToolbarSelectHeaderStyleButtonState @override void didUpdateWidget( - covariant QuillToolbarSelectHeaderStyleButton oldWidget) { + covariant QuillToolbarSelectHeaderStyleButtons oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { + if (oldWidget.controller != controller) { oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); _selectedAttribute = _getHeaderValue(); } } @override void dispose() { - widget.controller.removeListener(_didChangeEditingValue); + controller.removeListener(_didChangeEditingValue); super.dispose(); } } diff --git a/lib/src/widgets/toolbar/buttons/toggle_check_list.dart b/lib/src/widgets/toolbar/buttons/toggle_check_list.dart index eec49bde..189b9a30 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_check_list.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_check_list.dart @@ -31,18 +31,11 @@ class _QuillToolbarToggleCheckListButtonState extends State<QuillToolbarToggleCheckListButton> { bool? _isToggled; - /// Since it's not safe to call anything related to the context in dispose - /// then we will save a reference to the [controller] - /// and update it in [didChangeDependencies] - /// and use it in dispose method - late QuillController _controller; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); + Style get _selectionStyle => controller.getSelectionStyle(); void _didChangeEditingValue() { setState(() { - _isToggled = - _getIsToggled(widget.controller.getSelectionStyle().attributes); + _isToggled = _getIsToggled(controller.getSelectionStyle().attributes); }); } @@ -50,17 +43,17 @@ class _QuillToolbarToggleCheckListButtonState void initState() { super.initState(); _isToggled = _getIsToggled(_selectionStyle.attributes); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); } bool _getIsToggled(Map<String, Attribute> attrs) { - var attribute = widget.controller.toolbarButtonToggler[Attribute.list.key]; + var attribute = controller.toolbarButtonToggler[Attribute.list.key]; if (attribute == null) { attribute = attrs[Attribute.list.key]; } else { // checkbox tapping causes controller.selection to go to offset 0 - widget.controller.toolbarButtonToggler.remove(Attribute.list.key); + controller.toolbarButtonToggler.remove(Attribute.list.key); } if (attribute == null) { @@ -75,20 +68,14 @@ class _QuillToolbarToggleCheckListButtonState super.didUpdateWidget(oldWidget); if (oldWidget.controller != controller) { oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); _isToggled = _getIsToggled(_selectionStyle.attributes); } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _controller = controller; - } - @override void dispose() { - _controller.removeListener(_didChangeEditingValue); + controller.removeListener(_didChangeEditingValue); super.dispose(); } @@ -97,7 +84,7 @@ class _QuillToolbarToggleCheckListButtonState } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { diff --git a/lib/src/widgets/toolbar/buttons/toggle_style.dart b/lib/src/widgets/toolbar/buttons/toggle_style.dart index 4d965456..192dbb93 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_style.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_style.dart @@ -66,12 +66,6 @@ class QuillToolbarToggleStyleButton extends StatefulWidget { class _QuillToolbarToggleStyleButtonState extends State<QuillToolbarToggleStyleButton> { - /// Since it's not safe to call anything related to the context in dispose - /// then we will save a reference to the [controller] - /// and update it in [didChangeDependencies] - /// and use it in dispose method - late QuillController _controller; - bool? _isToggled; Style get _selectionStyle => controller.getSelectionStyle(); @@ -88,7 +82,7 @@ class _QuillToolbarToggleStyleButtonState } QuillController get controller { - return options.controller ?? widget.controller; + return widget.controller; } double get iconSize { @@ -236,20 +230,14 @@ class _QuillToolbarToggleStyleButtonState super.didUpdateWidget(oldWidget); if (oldWidget.controller != controller) { oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); + controller.addListener(_didChangeEditingValue); _isToggled = _getIsToggled(_selectionStyle.attributes); } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _controller = controller; - } - @override void dispose() { - _controller.removeListener(_didChangeEditingValue); + controller.removeListener(_didChangeEditingValue); super.dispose(); } diff --git a/lib/src/widgets/toolbar/enum.dart b/lib/src/widgets/toolbar/enum.dart index dee62abf..6f9e23a7 100644 --- a/lib/src/widgets/toolbar/enum.dart +++ b/lib/src/widgets/toolbar/enum.dart @@ -79,6 +79,9 @@ enum ToolbarButtons { 'You will find toolbarConfigurations and then buttons and pass a value' ' and change what you want, the tooltip for spesefic button for example') direction, + @Deprecated('Please customize the button in the QuillProvider. ' + 'You will find toolbarConfigurations and then buttons and pass a value' + ' and change what you want, the tooltip for spesefic button for example') headerStyle, @Deprecated('Please customize the button in the QuillProvider. ' 'You will find toolbarConfigurations and then buttons and pass a value' @@ -108,6 +111,12 @@ enum ToolbarButtons { 'You will find toolbarConfigurations and then buttons and pass a value' ' and change what you want, the tooltip for spesefic button for example') indentDecrease, + @Deprecated('Please customize the button in the QuillProvider. ' + 'You will find toolbarConfigurations and then buttons and pass a value' + ' and change what you want, the tooltip for spesefic button for example') link, + @Deprecated('Please customize the button in the QuillProvider. ' + 'You will find toolbarConfigurations and then buttons and pass a value' + ' and change what you want, the tooltip for spesefic button for example') search, } diff --git a/lib/src/widgets/toolbar/toolbar.dart b/lib/src/widgets/toolbar/toolbar.dart index 590b4b2b..b88da169 100644 --- a/lib/src/widgets/toolbar/toolbar.dart +++ b/lib/src/widgets/toolbar/toolbar.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:i18n_extension/i18n_widget.dart'; import '../../../flutter_quill.dart'; -import '../../translations/toolbar.i18n.dart'; import '../../utils/extensions/build_context.dart'; import 'buttons/arrow_indicated_list.dart'; @@ -18,7 +17,7 @@ export 'buttons/indent.dart'; export 'buttons/link_style.dart'; export 'buttons/link_style2.dart'; export 'buttons/quill_icon.dart'; -export 'buttons/search.dart'; +export 'buttons/search/search.dart'; export 'buttons/select_alignment.dart'; export 'buttons/select_header_style.dart'; export 'buttons/toggle_check_list.dart'; @@ -38,7 +37,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { this.toolbarIconCrossAlignment = WrapCrossAlignment.center, this.color, this.customButtons = const [], - VoidCallback? afterButtonPressed, this.sectionDividerColor, this.sectionDividerSpace, this.linkDialogAction, @@ -47,7 +45,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { }) : super(key: key); factory QuillToolbar.basic({ - Axis axis = Axis.horizontal, double toolbarSectionSpacing = kToolbarSectionSpacing, WrapAlignment toolbarIconAlignment = WrapAlignment.center, WrapCrossAlignment toolbarIconCrossAlignment = WrapCrossAlignment.center, @@ -97,10 +94,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ///shown when embedding an image, for example QuillDialogTheme? dialogTheme, - /// Callback to be called after any button on the toolbar is pressed. - /// Is called after whatever logic the button performs has run. - VoidCallback? afterButtonPressed, - ///Map of tooltips for toolbar buttons /// ///The example is: @@ -113,6 +106,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ///``` /// /// To disable tooltips just pass empty map as well. + @Deprecated('This is deprecated and will no longer used. ' + 'to change the tooltips please pass them in the Quill toolbar button' + ' configurations which exists in in the QuillProvider') Map<ToolbarButtons, String>? tooltips, /// The locale to use for the editor toolbar, defaults to system locale @@ -127,10 +123,6 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { /// The space occupied by toolbar divider double? sectionDividerSpace, - - /// Validate the legitimacy of hyperlinks - RegExp? linkRegExp, - LinkDialogAction? linkDialogAction, Key? key, }) { final isButtonGroupShown = [ @@ -157,40 +149,46 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { showLink || showSearchButton ]; - //default button tooltips - final buttonTooltips = tooltips ?? - <ToolbarButtons, String>{ - ToolbarButtons.headerStyle: 'Header style'.i18n, - ToolbarButtons.link: 'Insert URL'.i18n, - ToolbarButtons.search: 'Search'.i18n, - }; - return QuillToolbar( key: key, - axis: axis, color: color, decoration: decoration, toolbarSectionSpacing: toolbarSectionSpacing, toolbarIconAlignment: toolbarIconAlignment, toolbarIconCrossAlignment: toolbarIconCrossAlignment, customButtons: customButtons, - afterButtonPressed: afterButtonPressed, childrenBuilder: (context) { final controller = context.requireQuillController; final toolbarConfigurations = context.requireQuillToolbarConfigurations; - final toolbarIconSize = + final globalIconSize = toolbarConfigurations.buttonOptions.base.globalIconSize; + final axis = toolbarConfigurations.axis; + + if (tooltips != null) { + throw UnsupportedError( + 'This is deprecated and will no longer used. to change ' + 'the tooltips please pass them in the Quill toolbar button' + 'configurations which exists in in the QuillProvider', + ); + } + return [ if (showUndo) QuillToolbarHistoryButton( options: toolbarConfigurations.buttonOptions.undoHistory, + controller: + toolbarConfigurations.buttonOptions.undoHistory.controller ?? + context.requireQuillController, ), if (showRedo) QuillToolbarHistoryButton( options: toolbarConfigurations.buttonOptions.redoHistory, + controller: + toolbarConfigurations.buttonOptions.redoHistory.controller ?? + context.requireQuillController, ), if (showFontFamily) QuillToolbarFontFamilyButton( @@ -288,7 +286,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ), if (embedButtons != null) for (final builder in embedButtons) - builder(controller, toolbarIconSize, iconTheme, dialogTheme), + builder(controller, globalIconSize, iconTheme, dialogTheme), if (showDividers && isButtonGroupShown[0] && (isButtonGroupShown[1] || @@ -338,13 +336,15 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { space: sectionDividerSpace, ), if (showHeaderStyle) - QuillToolbarSelectHeaderStyleButton( - tooltip: buttonTooltips[ToolbarButtons.headerStyle], + QuillToolbarSelectHeaderStyleButtons( controller: controller, - axis: axis, - iconSize: toolbarIconSize, - iconTheme: iconTheme, - afterButtonPressed: afterButtonPressed, + options: + toolbarConfigurations.buttonOptions.selectHeaderStyleButtons, + // tooltip: buttonTooltips[ToolbarButtons.headerStyle], + // axis: axis, + // iconSize: toolbarIconSize, + // iconTheme: iconTheme, + // afterButtonPressed: afterButtonPressed, ), if (showDividers && showHeaderStyle && @@ -422,28 +422,13 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { color: sectionDividerColor, space: sectionDividerSpace), if (showLink) QuillToolbarLinkStyleButton( - tooltip: buttonTooltips[ToolbarButtons.link], controller: controller, - iconSize: toolbarIconSize, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - afterButtonPressed: afterButtonPressed, - linkRegExp: linkRegExp, - linkDialogAction: linkDialogAction, - dialogBarrierColor: - context.requireQuillSharedConfigurations.dialogBarrierColor, + options: toolbarConfigurations.buttonOptions.linkStyle, ), if (showSearchButton) QuillToolbarSearchButton( - icon: Icons.search, - iconSize: toolbarIconSize, - dialogBarrierColor: - context.requireQuillSharedConfigurations.dialogBarrierColor, - tooltip: buttonTooltips[ToolbarButtons.search], controller: controller, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - afterButtonPressed: afterButtonPressed, + options: toolbarConfigurations.buttonOptions.search, ), if (customButtons.isNotEmpty) if (showDividers) @@ -461,12 +446,16 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ] else ...[ CustomButton( onPressed: customButton.onTap, - icon: customButton.icon, + icon: customButton.iconData ?? + context.quillToolbarBaseButtonOptions?.iconData, iconColor: customButton.iconColor, - iconSize: toolbarIconSize, - iconTheme: iconTheme, - afterButtonPressed: afterButtonPressed, - tooltip: customButton.tooltip, + iconSize: customButton.iconSize ?? globalIconSize, + iconTheme: iconTheme ?? + context.quillToolbarBaseButtonOptions?.iconTheme, + afterButtonPressed: customButton.afterButtonPressed ?? + context.quillToolbarBaseButtonOptions?.afterButtonPressed, + tooltip: customButton.tooltip ?? + context.quillToolbarBaseButtonOptions?.tooltip, ), ], ]; diff --git a/pubspec.yaml b/pubspec.yaml index c569a468..b25bc38b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter. -version: 7.8.0 +version: 7.9.0 homepage: https://1o24bbs.com/c/bulletjournal/108 repository: https://github.com/singerdmx/flutter-quill topics: