mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support menu action in aa toolbar item (#4201)
This commit is contained in:
parent
c033d56978
commit
ce51491cf6
@ -1,8 +1,16 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _left = 'left';
|
||||
const _center = 'center';
|
||||
const _right = 'right';
|
||||
|
||||
class AlignItems extends StatelessWidget {
|
||||
const AlignItems({
|
||||
super.key,
|
||||
@ -13,18 +21,97 @@ class AlignItems extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MobileToolbarItemWrapper(
|
||||
size: const Size(82, 52),
|
||||
onTap: () async {
|
||||
await editorState.alignBlock('left');
|
||||
final currentAlignItem = _getCurrentAlignItem();
|
||||
final alignMenuItems = _getAlignMenuItems();
|
||||
return PopupMenu(
|
||||
itemLength: alignMenuItems.length,
|
||||
onSelected: (index) {
|
||||
editorState.alignBlock(alignMenuItems[index].$1);
|
||||
},
|
||||
icon: FlowySvgs.m_aa_align_left_s,
|
||||
isSelected: false,
|
||||
iconPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14.0,
|
||||
menuBuilder: (context, keys, currentIndex) {
|
||||
final children = alignMenuItems
|
||||
.mapIndexed(
|
||||
(index, e) => [
|
||||
MenuItem(
|
||||
key: keys[index],
|
||||
isSelected: currentIndex == index,
|
||||
icon: e.$2,
|
||||
),
|
||||
if (index != 0 || index != alignMenuItems.length - 1)
|
||||
const HSpace(12),
|
||||
],
|
||||
)
|
||||
.flattened
|
||||
.toList();
|
||||
return MenuWrapper(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, key) => MobileToolbarItemWrapper(
|
||||
key: key,
|
||||
size: const Size(82, 52),
|
||||
onTap: () async {
|
||||
await editorState.alignBlock(currentAlignItem.$1);
|
||||
},
|
||||
icon: currentAlignItem.$2,
|
||||
isSelected: false,
|
||||
iconPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14.0,
|
||||
),
|
||||
showDownArrow: true,
|
||||
backgroundColor: const Color(0xFFF2F2F7),
|
||||
),
|
||||
showDownArrow: true,
|
||||
backgroundColor: const Color(0xFFF2F2F7),
|
||||
);
|
||||
}
|
||||
|
||||
(String, FlowySvgData) _getCurrentAlignItem() {
|
||||
final align = _getCurrentBlockAlign();
|
||||
if (align == _center) {
|
||||
return (_center, FlowySvgs.m_aa_align_center_s);
|
||||
} else if (align == _right) {
|
||||
return (_right, FlowySvgs.m_aa_align_right_s);
|
||||
}
|
||||
return (_left, FlowySvgs.m_aa_align_left_s);
|
||||
}
|
||||
|
||||
List<(String, FlowySvgData)> _getAlignMenuItems() {
|
||||
final align = _getCurrentBlockAlign();
|
||||
|
||||
if (align == _center) {
|
||||
return [
|
||||
(_left, FlowySvgs.m_aa_align_left_s),
|
||||
(_right, FlowySvgs.m_aa_align_right_s),
|
||||
];
|
||||
} else if (align == _right) {
|
||||
return [
|
||||
(_left, FlowySvgs.m_aa_align_left_s),
|
||||
(_center, FlowySvgs.m_aa_align_center_s),
|
||||
];
|
||||
}
|
||||
return [
|
||||
(_center, FlowySvgs.m_aa_align_center_s),
|
||||
(_right, FlowySvgs.m_aa_align_right_s),
|
||||
];
|
||||
}
|
||||
|
||||
String _getCurrentBlockAlign() {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return _left;
|
||||
}
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
String? alignString;
|
||||
for (final node in nodes) {
|
||||
final align = node.attributes[blockComponentAlign];
|
||||
if (alignString == null) {
|
||||
alignString = align;
|
||||
} else if (alignString != align) {
|
||||
return _left;
|
||||
}
|
||||
}
|
||||
return alignString ?? _left;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@ -41,7 +44,6 @@ class BlockItems extends StatelessWidget {
|
||||
)
|
||||
.flattened,
|
||||
// this item is a special case, use link item here instead of block item
|
||||
|
||||
_buildLinkItem(),
|
||||
],
|
||||
),
|
||||
@ -72,19 +74,51 @@ class BlockItems extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildLinkItem() {
|
||||
return MobileToolbarItemWrapper(
|
||||
size: const Size(62, 54),
|
||||
enableTopLeftRadius: false,
|
||||
enableBottomLeftRadius: false,
|
||||
enableTopRightRadius: true,
|
||||
enableBottomRightRadius: true,
|
||||
showDownArrow: true,
|
||||
onTap: _onLinkItemTap,
|
||||
backgroundColor: const Color(0xFFF2F2F7),
|
||||
icon: FlowySvgs.m_aa_link_s,
|
||||
isSelected: false,
|
||||
iconPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14.0,
|
||||
final items = [
|
||||
(AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_s),
|
||||
// (InlineMathEquationKeys.formula, FlowySvgs.m_aa_math_s),
|
||||
];
|
||||
return PopupMenu(
|
||||
itemLength: items.length,
|
||||
onSelected: (index) async {
|
||||
await editorState.toggleAttribute(items[index].$1);
|
||||
},
|
||||
menuBuilder: (context, keys, currentIndex) {
|
||||
final children = items
|
||||
.mapIndexed(
|
||||
(index, e) => [
|
||||
MenuItem(
|
||||
key: keys[index],
|
||||
isSelected: currentIndex == index,
|
||||
icon: e.$2,
|
||||
),
|
||||
if (index != 0 || index != items.length - 1) const HSpace(12),
|
||||
],
|
||||
)
|
||||
.flattened
|
||||
.toList();
|
||||
return MenuWrapper(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, key) => MobileToolbarItemWrapper(
|
||||
key: key,
|
||||
size: const Size(62, 54),
|
||||
enableTopLeftRadius: false,
|
||||
enableBottomLeftRadius: false,
|
||||
enableTopRightRadius: true,
|
||||
enableBottomRightRadius: true,
|
||||
showDownArrow: true,
|
||||
onTap: _onLinkItemTap,
|
||||
backgroundColor: const Color(0xFFF2F2F7),
|
||||
icon: FlowySvgs.m_aa_link_s,
|
||||
isSelected: false,
|
||||
iconPadding: const EdgeInsets.symmetric(
|
||||
vertical: 14.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MenuItem extends StatelessWidget {
|
||||
const MenuItem({
|
||||
super.key,
|
||||
required this.isSelected,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final FlowySvgData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 62,
|
||||
height: 44,
|
||||
decoration: ShapeDecoration(
|
||||
color: isSelected ? const Color(0xFF00BCF0) : null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9),
|
||||
child: FlowySvg(
|
||||
icon,
|
||||
color: isSelected ? Colors.white : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuWrapper extends StatelessWidget {
|
||||
const MenuWrapper({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: ShapeDecoration(
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shadows: const [
|
||||
BoxShadow(
|
||||
color: Color(0x2D000000),
|
||||
blurRadius: 20,
|
||||
offset: Offset(0, 10),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class PopupMenu extends StatefulWidget {
|
||||
const PopupMenu({
|
||||
super.key,
|
||||
required this.onSelected,
|
||||
required this.itemLength,
|
||||
required this.menuBuilder,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Widget Function(BuildContext context, Key key) builder;
|
||||
final int itemLength;
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
List<GlobalKey> keys,
|
||||
int currentIndex,
|
||||
) menuBuilder;
|
||||
final void Function(int index) onSelected;
|
||||
|
||||
@override
|
||||
State<PopupMenu> createState() => _PopupMenuState();
|
||||
}
|
||||
|
||||
class _PopupMenuState extends State<PopupMenu> {
|
||||
final key = GlobalKey();
|
||||
final indexNotifier = ValueNotifier(-1);
|
||||
late List<GlobalKey> itemKeys;
|
||||
|
||||
OverlayEntry? popupMenuOverlayEntry;
|
||||
|
||||
Rect get rect {
|
||||
final RenderBox renderBox =
|
||||
key.currentContext!.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
return Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
indexNotifier.value = widget.itemLength - 1;
|
||||
itemKeys = List.generate(
|
||||
widget.itemLength,
|
||||
(_) => GlobalKey(),
|
||||
);
|
||||
|
||||
indexNotifier.addListener(HapticFeedback.mediumImpact);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
indexNotifier.removeListener(HapticFeedback.mediumImpact);
|
||||
indexNotifier.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPressStart: (details) {
|
||||
_showMenu(context);
|
||||
},
|
||||
onLongPressMoveUpdate: (details) {
|
||||
_updateSelection(details);
|
||||
},
|
||||
onLongPressCancel: () {
|
||||
_hideMenu();
|
||||
},
|
||||
onLongPressUp: () {
|
||||
if (indexNotifier.value != -1) {
|
||||
widget.onSelected(indexNotifier.value);
|
||||
}
|
||||
_hideMenu();
|
||||
},
|
||||
child: widget.builder(context, key),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMenu(BuildContext context) {
|
||||
_hideMenu();
|
||||
|
||||
indexNotifier.value = widget.itemLength - 1;
|
||||
popupMenuOverlayEntry ??= OverlayEntry(
|
||||
builder: (context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final right = screenSize.width - rect.right;
|
||||
final bottom = screenSize.height - rect.top + 16;
|
||||
return Positioned(
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: indexNotifier,
|
||||
builder: (context, value, _) => widget.menuBuilder(
|
||||
context,
|
||||
itemKeys,
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(context).insert(popupMenuOverlayEntry!);
|
||||
}
|
||||
|
||||
void _hideMenu() {
|
||||
indexNotifier.value = -1;
|
||||
|
||||
popupMenuOverlayEntry?.remove();
|
||||
popupMenuOverlayEntry = null;
|
||||
}
|
||||
|
||||
void _updateSelection(LongPressMoveUpdateDetails details) {
|
||||
final dx = details.globalPosition.dx;
|
||||
for (var i = 0; i < itemKeys.length; i++) {
|
||||
final key = itemKeys[i];
|
||||
final RenderBox renderBox =
|
||||
key.currentContext!.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final rect = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
||||
// ignore the position overflow
|
||||
if ((i == 0 && dx < rect.left) ||
|
||||
(i == itemKeys.length - 1 && dx > rect.right)) {
|
||||
indexNotifier.value = -1;
|
||||
break;
|
||||
}
|
||||
if (rect.left <= dx && dx <= rect.right) {
|
||||
indexNotifier.value = itemKeys.indexOf(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -267,8 +267,8 @@ class EditorStyleCustomizer {
|
||||
}
|
||||
|
||||
// customize the inline math equation block
|
||||
final formula = attributes[InlineMathEquationKeys.formula] as String?;
|
||||
if (formula != null) {
|
||||
final formula = attributes[InlineMathEquationKeys.formula];
|
||||
if (formula is String) {
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: InlineMathEquation(
|
||||
|
5
frontend/resources/flowy_icons/16x/m_aa_align_center.svg
Normal file
5
frontend/resources/flowy_icons/16x/m_aa_align_center.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.75 5.57904C3.75 5.07047 4.16227 4.6582 4.67083 4.6582H21.2458C21.7544 4.6582 22.1667 5.07047 22.1667 5.57904C22.1667 6.0876 21.7544 6.49987 21.2458 6.49987H4.67083C4.16227 6.49987 3.75 6.0876 3.75 5.57904Z" fill="#2B2F36"/>
|
||||
<path d="M3.75 20.3124C3.75 19.8038 4.16227 19.3915 4.67083 19.3915H21.2458C21.7544 19.3915 22.1667 19.8038 22.1667 20.3124C22.1667 20.8209 21.7544 21.2332 21.2458 21.2332H4.67083C4.16227 21.2332 3.75 20.8209 3.75 20.3124Z" fill="#2B2F36"/>
|
||||
<path d="M9.275 12.0249C8.76644 12.0249 8.35417 12.4371 8.35417 12.9457C8.35417 13.4543 8.76644 13.8665 9.275 13.8665H16.6417C17.1502 13.8665 17.5625 13.4543 17.5625 12.9457C17.5625 12.4371 17.1502 12.0249 16.6417 12.0249H9.275Z" fill="#2B2F36"/>
|
||||
</svg>
|
After Width: | Height: | Size: 827 B |
5
frontend/resources/flowy_icons/16x/m_aa_align_right.svg
Normal file
5
frontend/resources/flowy_icons/16x/m_aa_align_right.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.41602 5.68743C3.41602 5.17887 3.82829 4.7666 4.33685 4.7666H20.9118C21.4204 4.7666 21.8327 5.17887 21.8327 5.68743C21.8327 6.196 21.4204 6.60827 20.9118 6.60827H4.33685C3.82829 6.60827 3.41602 6.196 3.41602 5.68743Z" fill="#2B2F36"/>
|
||||
<path d="M3.41602 20.4208C3.41602 19.9122 3.82829 19.4999 4.33685 19.4999H20.9118C21.4204 19.4999 21.8327 19.9122 21.8327 20.4208C21.8327 20.9293 21.4204 21.3416 20.9118 21.3416H4.33685C3.82829 21.3416 3.41602 20.9293 3.41602 20.4208Z" fill="#2B2F36"/>
|
||||
<path d="M12.6243 12.1333C12.1158 12.1333 11.7035 12.5455 11.7035 13.0541C11.7035 13.5627 12.1158 13.9749 12.6243 13.9749H20.9118C21.4204 13.9749 21.8327 13.5627 21.8327 13.0541C21.8327 12.5455 21.4204 12.1333 20.9118 12.1333H12.6243Z" fill="#2B2F36"/>
|
||||
</svg>
|
After Width: | Height: | Size: 855 B |
5
frontend/resources/flowy_icons/16x/m_aa_code.svg
Normal file
5
frontend/resources/flowy_icons/16x/m_aa_code.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8139 3.25614C14.3685 3.20145 13.9631 3.51818 13.9084 3.96356L11.928 20.0924C11.8733 20.5378 12.1901 20.9432 12.6354 20.9979C13.0808 21.0526 13.4862 20.7359 13.5409 20.2905L15.5213 4.1616C15.576 3.71622 15.2592 3.31083 14.8139 3.25614Z" fill="#2B2F36"/>
|
||||
<path d="M10.6683 6.67747C10.9856 6.99478 10.9856 7.50922 10.6683 7.82652L5.96155 12.5332L10.6683 17.24C10.9856 17.5573 10.9856 18.0717 10.6683 18.389C10.351 18.7063 9.83653 18.7063 9.51923 18.389L4.23798 13.1078C3.92067 12.7905 3.92067 12.276 4.23798 11.9587L9.51923 6.67747C9.83653 6.36017 10.351 6.36017 10.6683 6.67747Z" fill="#2B2F36"/>
|
||||
<path d="M16.8317 6.67747C16.5144 6.99478 16.5144 7.50922 16.8317 7.82652L21.5385 12.5332L16.8317 17.24C16.5144 17.5573 16.5144 18.0717 16.8317 18.389C17.149 18.7063 17.6635 18.7063 17.9808 18.389L23.262 13.1078C23.5793 12.7905 23.5793 12.276 23.262 11.9587L17.9808 6.67747C17.6635 6.36017 17.149 6.36017 16.8317 6.67747Z" fill="#2B2F36"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
3
frontend/resources/flowy_icons/16x/m_aa_math.svg
Normal file
3
frontend/resources/flowy_icons/16x/m_aa_math.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.85695 7.70721C5.8651 6.98586 6.37535 5.41675 7.60177 5.41675H20.6555C21.1217 5.41675 21.4997 5.79472 21.4997 6.26097C21.4997 6.72722 21.1217 7.10519 20.6555 7.10519H8.89983L15.6173 11.9906C16.3126 12.4963 16.3126 13.5332 15.6173 14.0389L8.89983 18.9243H20.6555C21.1217 18.9243 21.4997 19.3023 21.4997 19.7685C21.4997 20.2348 21.1217 20.6127 20.6555 20.6127H7.60177C6.37535 20.6127 5.8651 19.0436 6.85695 18.3223L14.1548 13.0147L6.85695 7.70721Z" fill="#2B2F36"/>
|
||||
</svg>
|
After Width: | Height: | Size: 578 B |
Loading…
Reference in New Issue
Block a user