mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
[flutter]: flowy toolbar
This commit is contained in:
parent
8e5cbfb84e
commit
e4a3355e75
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
@ -9,6 +9,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import 'widget/toolbar/tool_bar.dart';
|
||||
|
||||
class DocPage extends StatefulWidget {
|
||||
final View view;
|
||||
|
||||
@ -63,7 +65,7 @@ class _DocPageState extends State<DocPage> {
|
||||
_renderEditor(controller),
|
||||
_renderToolbar(controller),
|
||||
],
|
||||
).padding(horizontal: 80, vertical: 48);
|
||||
).padding(horizontal: 80, top: 48);
|
||||
}
|
||||
|
||||
Widget _renderEditor(QuillController controller) {
|
||||
@ -84,14 +86,10 @@ class _DocPageState extends State<DocPage> {
|
||||
}
|
||||
|
||||
Widget _renderToolbar(QuillController controller) {
|
||||
return QuillToolbar.basic(
|
||||
return EditorToolbar.basic(
|
||||
controller: controller,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _onImageSelection(File file) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
// import 'package:flowy_editor/flowy_editor.dart';
|
||||
|
@ -0,0 +1,93 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:editor/models/documents/style.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FlowyCheckListButton extends StatefulWidget {
|
||||
const FlowyCheckListButton({
|
||||
required this.controller,
|
||||
required this.attribute,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.fillColor,
|
||||
this.childBuilder = defaultToggleStyleButtonBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final double iconSize;
|
||||
|
||||
final Color? fillColor;
|
||||
|
||||
final QuillController controller;
|
||||
|
||||
final ToggleStyleButtonBuilder childBuilder;
|
||||
|
||||
final Attribute attribute;
|
||||
|
||||
@override
|
||||
_FlowyCheckListButtonState createState() => _FlowyCheckListButtonState();
|
||||
}
|
||||
|
||||
class _FlowyCheckListButtonState extends State<FlowyCheckListButton> {
|
||||
bool? _isToggled;
|
||||
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() {
|
||||
_isToggled = _getIsToggled(widget.controller.getSelectionStyle().attributes);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
bool _getIsToggled(Map<String, Attribute> attrs) {
|
||||
if (widget.attribute.key == Attribute.list.key) {
|
||||
final attribute = attrs[widget.attribute.key];
|
||||
if (attribute == null) {
|
||||
return false;
|
||||
}
|
||||
return attribute.value == widget.attribute.value || attribute.value == Attribute.checked.value;
|
||||
}
|
||||
return attrs.containsKey(widget.attribute.key);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyCheckListButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FlowyIconButton(
|
||||
onPressed: _toggleAttribute,
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
icon: svg('editor/checkbox'),
|
||||
highlightColor: _isToggled == true ? theme.shader5 : theme.shader6,
|
||||
hoverColor: theme.shader5,
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleAttribute() {
|
||||
widget.controller.formatSelection(_isToggled! ? Attribute.clone(Attribute.unchecked, null) : Attribute.unchecked);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class FlowyImageButton extends StatelessWidget {
|
||||
const FlowyImageButton({
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
this.onImagePickCallback,
|
||||
this.fillColor,
|
||||
this.filePickImpl,
|
||||
this.webImagePickImpl,
|
||||
this.mediaPickSettingSelector,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final double iconSize;
|
||||
|
||||
final Color? fillColor;
|
||||
|
||||
final QuillController controller;
|
||||
|
||||
final OnImagePickCallback? onImagePickCallback;
|
||||
|
||||
final WebImagePickImpl? webImagePickImpl;
|
||||
|
||||
final FilePickImpl? filePickImpl;
|
||||
|
||||
final MediaPickSettingSelector? mediaPickSettingSelector;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FlowyIconButton(
|
||||
icon: svg('editor/image'),
|
||||
width: iconSize * 1.77,
|
||||
highlightColor: theme.canvasColor,
|
||||
onPressed: () => _onPressedHandler(context),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
if (onImagePickCallback != null) {
|
||||
final selector = mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
|
||||
final source = await selector(context);
|
||||
if (source != null) {
|
||||
if (source == MediaPickSetting.Gallery) {
|
||||
_pickImage(context);
|
||||
} else {
|
||||
_typeLink(context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_typeLink(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(
|
||||
context,
|
||||
controller,
|
||||
ImageSource.gallery,
|
||||
onImagePickCallback!,
|
||||
filePickImpl: filePickImpl,
|
||||
webImagePickImpl: webImagePickImpl,
|
||||
);
|
||||
|
||||
void _typeLink(BuildContext context) {
|
||||
// showDialog<String>(
|
||||
// context: context,
|
||||
// builder: (_) => const LinkDialog(),
|
||||
// ).then(_linkSubmitted);
|
||||
}
|
||||
|
||||
void _linkSubmitted(String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final index = controller.selection.baseOffset;
|
||||
final length = controller.selection.extentOffset - index;
|
||||
|
||||
controller.replaceText(index, length, BlockEmbed.image(value), null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FlowyLinkStyleButton extends StatefulWidget {
|
||||
const FlowyLinkStyleButton({
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final QuillController controller;
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
_FlowyLinkStyleButtonState createState() => _FlowyLinkStyleButtonState();
|
||||
}
|
||||
|
||||
class _FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
|
||||
void _didChangeSelection() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_didChangeSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyLinkStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeSelection);
|
||||
widget.controller.addListener(_didChangeSelection);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
widget.controller.removeListener(_didChangeSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEnabled = !widget.controller.selection.isCollapsed;
|
||||
final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null;
|
||||
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return FlowyIconButton(
|
||||
onPressed: pressedHandler,
|
||||
icon: svg('editor/share'),
|
||||
highlightColor: isEnabled == true ? theme.shader5 : theme.shader6,
|
||||
hoverColor: theme.shader5,
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
);
|
||||
}
|
||||
|
||||
void _openLinkDialog(BuildContext context) {
|
||||
// showDialog<String>(
|
||||
// context: context,
|
||||
// builder: (ctx) {
|
||||
// return const LinkDialog();
|
||||
// },
|
||||
// ).then(_linkSubmitted);
|
||||
}
|
||||
|
||||
void _linkSubmitted(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
widget.controller.formatSelection(LinkAttribute(value));
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:editor/models/documents/style.dart';
|
||||
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FlowyToggleStyleButton extends StatefulWidget {
|
||||
final Attribute attribute;
|
||||
final Widget icon;
|
||||
final double iconSize;
|
||||
final QuillController controller;
|
||||
|
||||
const FlowyToggleStyleButton({
|
||||
required this.attribute,
|
||||
required this.icon,
|
||||
required this.controller,
|
||||
this.iconSize = kDefaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ToggleStyleButtonState createState() => _ToggleStyleButtonState();
|
||||
}
|
||||
|
||||
class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
|
||||
bool? _isToggled;
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return FlowyIconButton(
|
||||
onPressed: _toggleAttribute,
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
icon: widget.icon,
|
||||
highlightColor: _isToggled == true ? theme.shader5 : theme.shader6,
|
||||
hoverColor: theme.shader5,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyToggleStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
|
||||
}
|
||||
|
||||
bool _getIsToggled(Map<String, Attribute> attrs) {
|
||||
if (widget.attribute.key == Attribute.list.key) {
|
||||
final attribute = attrs[widget.attribute.key];
|
||||
if (attribute == null) {
|
||||
return false;
|
||||
}
|
||||
return attribute.value == widget.attribute.value;
|
||||
}
|
||||
return attrs.containsKey(widget.attribute.key);
|
||||
}
|
||||
|
||||
void _toggleAttribute() {
|
||||
widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute);
|
||||
}
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:editor/flutter_quill.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'check_button.dart';
|
||||
import 'image_button.dart';
|
||||
import 'link_button.dart';
|
||||
import 'toggle_button.dart';
|
||||
|
||||
class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final List<Widget> children;
|
||||
final double toolBarHeight;
|
||||
final Color? color;
|
||||
|
||||
const EditorToolbar({
|
||||
required this.children,
|
||||
this.toolBarHeight = 36,
|
||||
this.color,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).canvasColor,
|
||||
constraints: BoxConstraints.tightFor(height: preferredSize.height),
|
||||
child: ToolbarButtonList(buttons: children),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(toolBarHeight);
|
||||
|
||||
factory EditorToolbar.basic({
|
||||
required QuillController controller,
|
||||
double toolbarIconSize = kDefaultIconSize,
|
||||
OnImagePickCallback? onImagePickCallback,
|
||||
OnVideoPickCallback? onVideoPickCallback,
|
||||
MediaPickSettingSelector? mediaPickSettingSelector,
|
||||
FilePickImpl? filePickImpl,
|
||||
WebImagePickImpl? webImagePickImpl,
|
||||
WebVideoPickImpl? webVideoPickImpl,
|
||||
Key? key,
|
||||
}) {
|
||||
return EditorToolbar(
|
||||
key: key,
|
||||
toolBarHeight: toolbarIconSize * 2,
|
||||
children: [
|
||||
HistoryButton(
|
||||
icon: Icons.undo_outlined,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
undo: true,
|
||||
),
|
||||
HistoryButton(
|
||||
icon: Icons.redo_outlined,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
undo: false,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.bold,
|
||||
icon: svg('editor/bold'),
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.italic,
|
||||
icon: svg("editor/restore"),
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.underline,
|
||||
icon: svg('editor/underline'),
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.strikeThrough,
|
||||
icon: svg('editor/strikethrough'),
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
),
|
||||
ColorButton(
|
||||
icon: Icons.format_color_fill,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
background: true,
|
||||
),
|
||||
FlowyImageButton(
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
onImagePickCallback: onImagePickCallback,
|
||||
filePickImpl: filePickImpl,
|
||||
webImagePickImpl: webImagePickImpl,
|
||||
mediaPickSettingSelector: mediaPickSettingSelector,
|
||||
),
|
||||
SelectHeaderStyleButton(
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.ol,
|
||||
controller: controller,
|
||||
icon: svg('editor/numbers'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.ul,
|
||||
controller: controller,
|
||||
icon: svg('editor/bullet_list'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyCheckListButton(
|
||||
attribute: Attribute.unchecked,
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.inlineCode,
|
||||
controller: controller,
|
||||
icon: svg('editor/inline_block'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.blockQuote,
|
||||
controller: controller,
|
||||
icon: svg('editor/quote'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.blockQuote,
|
||||
controller: controller,
|
||||
icon: svg('editor/quote'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.blockQuote,
|
||||
controller: controller,
|
||||
icon: svg('editor/quote'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.blockQuote,
|
||||
controller: controller,
|
||||
icon: svg('editor/quote'),
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyLinkStyleButton(
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarButtonList extends StatefulWidget {
|
||||
const ToolbarButtonList({required this.buttons, Key? key}) : super(key: key);
|
||||
|
||||
final List<Widget> buttons;
|
||||
|
||||
@override
|
||||
_ToolbarButtonListState createState() => _ToolbarButtonListState();
|
||||
}
|
||||
|
||||
class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindingObserver {
|
||||
final ScrollController _controller = ScrollController();
|
||||
bool _showLeftArrow = false;
|
||||
bool _showRightArrow = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_handleScroll);
|
||||
|
||||
// Listening to the WidgetsBinding instance is necessary so that we can
|
||||
// hide the arrows when the window gets a new size and thus the toolbar
|
||||
// becomes scrollable/unscrollable.
|
||||
WidgetsBinding.instance!.addObserver(this);
|
||||
|
||||
// Workaround to allow the scroll controller attach to our ListView so that
|
||||
// we can detect if overflow arrows need to be shown on init.
|
||||
Timer.run(_handleScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return SizedBox(
|
||||
width: min(constraints.maxWidth, (widget.buttons.length + 3) * kDefaultIconSize * kIconButtonFactor + 16),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_buildLeftArrow(),
|
||||
_buildScrollableList(constraints),
|
||||
_buildRightColor(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() => _handleScroll();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleScroll() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels;
|
||||
_showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildLeftArrow() {
|
||||
return SizedBox(
|
||||
width: 8,
|
||||
child: Transform.translate(
|
||||
// Move the icon a few pixels to center it
|
||||
offset: const Offset(-5, 0),
|
||||
child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScrollableList(BoxConstraints constraints) {
|
||||
return ScrollConfiguration(
|
||||
// Remove the glowing effect, as we already have the arrow indicators
|
||||
behavior: _NoGlowBehavior(),
|
||||
// The CustomScrollView is necessary so that the children are not
|
||||
// stretched to the height of the toolbar, https://bit.ly/3uC3bjI
|
||||
child: Expanded(
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _controller,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return widget.buttons[index];
|
||||
},
|
||||
childCount: widget.buttons.length,
|
||||
addAutomaticKeepAlives: false,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRightColor() {
|
||||
return SizedBox(
|
||||
width: 8,
|
||||
child: Transform.translate(
|
||||
// Move the icon a few pixels to center it
|
||||
offset: const Offset(-5, 0),
|
||||
child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ScrollBehavior without the Material glow effect.
|
||||
class _NoGlowBehavior extends ScrollBehavior {
|
||||
@override
|
||||
Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) {
|
||||
return child;
|
||||
}
|
||||
}
|
@ -35,16 +35,15 @@ export 'toolbar/select_header_style_button.dart';
|
||||
export 'toolbar/toggle_check_list_button.dart';
|
||||
export 'toolbar/toggle_style_button.dart';
|
||||
export 'toolbar/video_button.dart';
|
||||
export 'toolbar/image_video_utils.dart';
|
||||
export 'toolbar/arrow_indicated_button_list.dart';
|
||||
|
||||
typedef OnImagePickCallback = Future<String?> Function(File file);
|
||||
typedef OnVideoPickCallback = Future<String?> Function(File file);
|
||||
typedef FilePickImpl = Future<String?> Function(BuildContext context);
|
||||
typedef WebImagePickImpl = Future<String?> Function(
|
||||
OnImagePickCallback onImagePickCallback);
|
||||
typedef WebVideoPickImpl = Future<String?> Function(
|
||||
OnVideoPickCallback onImagePickCallback);
|
||||
typedef MediaPickSettingSelector = Future<MediaPickSetting?> Function(
|
||||
BuildContext context);
|
||||
typedef WebImagePickImpl = Future<String?> Function(OnImagePickCallback onImagePickCallback);
|
||||
typedef WebVideoPickImpl = Future<String?> Function(OnVideoPickCallback onImagePickCallback);
|
||||
typedef MediaPickSettingSelector = Future<MediaPickSetting?> Function(BuildContext context);
|
||||
|
||||
// The default size of the icon of a button.
|
||||
const double kDefaultIconSize = 18;
|
||||
@ -218,8 +217,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
webVideoPickImpl: webImagePickImpl,
|
||||
mediaPickSettingSelector: mediaPickSettingSelector,
|
||||
),
|
||||
if ((onImagePickCallback != null || onVideoPickCallback != null) &&
|
||||
showCameraButton)
|
||||
if ((onImagePickCallback != null || onVideoPickCallback != null) && showCameraButton)
|
||||
CameraButton(
|
||||
icon: Icons.photo_camera,
|
||||
iconSize: toolbarIconSize,
|
||||
@ -246,10 +244,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
if (isButtonGroupShown[1] &&
|
||||
(isButtonGroupShown[2] ||
|
||||
isButtonGroupShown[3] ||
|
||||
isButtonGroupShown[4] ||
|
||||
isButtonGroupShown[5]))
|
||||
(isButtonGroupShown[2] || isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5]))
|
||||
VerticalDivider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
@ -260,10 +255,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
if (isButtonGroupShown[2] &&
|
||||
(isButtonGroupShown[3] ||
|
||||
isButtonGroupShown[4] ||
|
||||
isButtonGroupShown[5]))
|
||||
if (isButtonGroupShown[2] && (isButtonGroupShown[3] || isButtonGroupShown[4] || isButtonGroupShown[5]))
|
||||
VerticalDivider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
@ -297,8 +289,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
icon: Icons.code,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
if (isButtonGroupShown[3] &&
|
||||
(isButtonGroupShown[4] || isButtonGroupShown[5]))
|
||||
if (isButtonGroupShown[3] && (isButtonGroupShown[4] || isButtonGroupShown[5]))
|
||||
VerticalDivider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
|
@ -7,18 +7,15 @@ import 'package:flutter/material.dart';
|
||||
/// The arrow indicators are automatically hidden if the list is not
|
||||
/// scrollable in the direction of the respective arrow.
|
||||
class ArrowIndicatedButtonList extends StatefulWidget {
|
||||
const ArrowIndicatedButtonList({required this.buttons, Key? key})
|
||||
: super(key: key);
|
||||
const ArrowIndicatedButtonList({required this.buttons, Key? key}) : super(key: key);
|
||||
|
||||
final List<Widget> buttons;
|
||||
|
||||
@override
|
||||
_ArrowIndicatedButtonListState createState() =>
|
||||
_ArrowIndicatedButtonListState();
|
||||
_ArrowIndicatedButtonListState createState() => _ArrowIndicatedButtonListState();
|
||||
}
|
||||
|
||||
class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
|
||||
with WidgetsBindingObserver {
|
||||
class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList> with WidgetsBindingObserver {
|
||||
final ScrollController _controller = ScrollController();
|
||||
bool _showLeftArrow = false;
|
||||
bool _showRightArrow = false;
|
||||
@ -63,10 +60,8 @@ class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_showLeftArrow =
|
||||
_controller.position.minScrollExtent != _controller.position.pixels;
|
||||
_showRightArrow =
|
||||
_controller.position.maxScrollExtent != _controller.position.pixels;
|
||||
_showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels;
|
||||
_showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,7 @@ class SelectHeaderStyleButton extends StatefulWidget {
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
_SelectHeaderStyleButtonState createState() =>
|
||||
_SelectHeaderStyleButtonState();
|
||||
_SelectHeaderStyleButtonState createState() => _SelectHeaderStyleButtonState();
|
||||
}
|
||||
|
||||
class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
|
||||
@ -30,8 +29,7 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
setState(() {
|
||||
_value =
|
||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
_value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
});
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
@ -45,12 +43,7 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
|
||||
Attribute.h3: 'H3',
|
||||
};
|
||||
|
||||
final _valueAttribute = <Attribute>[
|
||||
Attribute.header,
|
||||
Attribute.h1,
|
||||
Attribute.h2,
|
||||
Attribute.h3
|
||||
];
|
||||
final _valueAttribute = <Attribute>[Attribute.header, Attribute.h1, Attribute.h2, Attribute.h3];
|
||||
final _valueString = <String>['N', 'H1', 'H2', 'H3'];
|
||||
|
||||
final theme = Theme.of(context);
|
||||
@ -74,13 +67,9 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
fillColor: _valueToText[_value] == _valueString[index]
|
||||
? theme.toggleableActiveColor
|
||||
: theme.canvasColor,
|
||||
onPressed: () =>
|
||||
widget.controller.formatSelection(_valueAttribute[index]),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
fillColor: _valueToText[_value] == _valueString[index] ? theme.toggleableActiveColor : theme.canvasColor,
|
||||
onPressed: () => widget.controller.formatSelection(_valueAttribute[index]),
|
||||
child: Text(
|
||||
_valueString[index],
|
||||
style: style.copyWith(
|
||||
@ -98,8 +87,7 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() {
|
||||
_value =
|
||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
_value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
});
|
||||
}
|
||||
|
||||
@ -109,8 +97,7 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_value =
|
||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
_value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,9 +99,7 @@ class _ToggleStyleButtonState extends State<ToggleStyleButton> {
|
||||
}
|
||||
|
||||
void _toggleAttribute() {
|
||||
widget.controller.formatSelection(_isToggled!
|
||||
? Attribute.clone(widget.attribute, null)
|
||||
: widget.attribute);
|
||||
widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,9 +119,7 @@ Widget defaultToggleStyleButtonBuilder(
|
||||
? theme.primaryIconTheme.color
|
||||
: theme.iconTheme.color
|
||||
: theme.disabledColor;
|
||||
final fill = isToggled == true
|
||||
? theme.toggleableActiveColor
|
||||
: fillColor ?? theme.canvasColor;
|
||||
final fill = isToggled == true ? theme.toggleableActiveColor : fillColor ?? theme.canvasColor;
|
||||
return QuillIconButton(
|
||||
highlightElevation: 0,
|
||||
hoverElevation: 0,
|
||||
|
@ -117,7 +117,11 @@ class AppTheme {
|
||||
ThemeData get themeData {
|
||||
var t = ThemeData(
|
||||
textTheme: (isDark ? ThemeData.dark() : ThemeData.light()).textTheme,
|
||||
textSelectionTheme: TextSelectionThemeData(cursorColor: main1),
|
||||
textSelectionTheme: TextSelectionThemeData(cursorColor: main2),
|
||||
primaryIconTheme: IconThemeData(color: hover),
|
||||
iconTheme: IconThemeData(color: shader1),
|
||||
canvasColor: shader6,
|
||||
// hoverColor: hover,
|
||||
colorScheme: ColorScheme(
|
||||
brightness: isDark ? Brightness.dark : Brightness.light,
|
||||
primary: main1,
|
||||
@ -141,6 +145,5 @@ class AppTheme {
|
||||
toggleableActiveColor: main1);
|
||||
}
|
||||
|
||||
Color shift(Color c, double d) =>
|
||||
ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
|
||||
Color shift(Color c, double d) => ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
|
||||
}
|
||||
|
@ -6,26 +6,41 @@ class FlowyIconButton extends StatelessWidget {
|
||||
final double? height;
|
||||
final Widget icon;
|
||||
final VoidCallback? onPressed;
|
||||
final Color? highlightColor;
|
||||
final Color? hoverColor;
|
||||
final EdgeInsets iconPadding;
|
||||
|
||||
const FlowyIconButton({
|
||||
Key? key,
|
||||
this.height,
|
||||
this.onPressed,
|
||||
this.width = 30,
|
||||
this.highlightColor = Colors.transparent,
|
||||
this.hoverColor = Colors.transparent,
|
||||
this.iconPadding = const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
required this.icon,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height ?? width,
|
||||
child: IconButton(
|
||||
icon: icon,
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: width,
|
||||
alignment: Alignment.center,
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(width: width, height: width),
|
||||
child: RawMaterialButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
hoverElevation: 0,
|
||||
highlightElevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
fillColor: highlightColor,
|
||||
hoverColor: hoverColor,
|
||||
focusColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
onPressed: onPressed,
|
||||
child: Padding(
|
||||
padding: iconPadding,
|
||||
child: SizedBox.fromSize(child: icon, size: Size(width, width)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user