feat: optimize editing experience on mobile (#592)

* feat: optimize mobile editing

* feat: refactot the mobile toolbar

* feat: don't reattch text input if the selection doesn't change

* chore: update mobile toolbar layout
This commit is contained in:
Lucas.Xu 2023-11-15 16:03:17 +08:00 • committed by GitHub
commit 4345105ebe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 824 additions and 142 deletions

View file

@ -87,13 +87,14 @@ class _HomePageState extends State<HomePage> {
key: _scaffoldKey,
extendBodyBehindAppBar: PlatformExtension.isDesktopOrWeb,
drawer: _buildDrawer(context),
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 134, 46, 247),
foregroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
title: const Text('AppFlowy Editor'),
),
body: SafeArea(child: _buildBody(context)),
body: _buildBody(context),
);
}

View file

@ -90,18 +90,14 @@ class _MobileEditorState extends State<MobileEditor> {
),
),
// build mobile toolbar
MobileToolbar(
MobileToolbarV2(
editorState: editorState,
toolbarItems: [
textDecorationMobileToolbarItem,
textDecorationMobileToolbarItemV2,
buildTextAndBackgroundColorMobileToolbarItem(),
headingMobileToolbarItem,
todoListMobileToolbarItem,
listMobileToolbarItem,
blocksMobileToolbarItem,
linkMobileToolbarItem,
quoteMobileToolbarItem,
dividerMobileToolbarItem,
codeMobileToolbarItem,
],
),
],

View file

@ -33,6 +33,9 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
late final TextInputService textInputService;
late final FocusNode focusNode;
// previous selection
Selection? previousSelection;
// use for IME only
bool enableShortcuts = true;
@ -187,9 +190,17 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
if (doNotAttach == true) {
return;
}
enableShortcuts = true;
// attach the delta text input service if needed
final selection = editorState.selection;
if (PlatformExtension.isMobile && previousSelection == selection) {
// no need to attach the text input service if the selection is not changed.
return;
}
enableShortcuts = true;
if (selection == null) {
textInputService.close();
} else {
@ -202,6 +213,8 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
Log.editor.debug('keyboard service - request focus');
}
}
previousSelection = selection;
}
void _attachTextInputService(Selection selection) {

View file

@ -2,5 +2,6 @@ export 'mobile_floating_toolbar/mobile_floating_toolbar.dart';
export 'mobile_toolbar.dart';
export 'mobile_toolbar_item.dart';
export 'mobile_toolbar_style.dart';
export 'mobile_toolbar_v2.dart';
export 'toolbar_items/toolbar_items.dart';
export 'utils/utils.dart';

View file

@ -53,14 +53,14 @@ class MobileToolbar extends StatelessWidget {
return const SizedBox.shrink();
}
return RepaintBoundary(
child: MobileToolbarStyle(
child: MobileToolbarTheme(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
clearDiagonalLineColor: clearDiagonalLineColor,
itemHighlightColor: itemHighlightColor,
itemOutlineColor: itemOutlineColor,
tabbarSelectedBackgroundColor: tabbarSelectedBackgroundColor,
tabbarSelectedForegroundColor: tabbarSelectedForegroundColor,
tabBarSelectedBackgroundColor: tabbarSelectedBackgroundColor,
tabBarSelectedForegroundColor: tabbarSelectedForegroundColor,
primaryColor: primaryColor,
onPrimaryColor: onPrimaryColor,
outlineColor: outlineColor,
@ -90,7 +90,7 @@ abstract class MobileToolbarWidgetService {
void closeItemMenu();
}
class MobileToolbarWidget extends StatefulWidget {
class MobileToolbarWidget extends StatefulWidget with WidgetsBindingObserver {
const MobileToolbarWidget({
super.key,
required this.editorState,
@ -111,6 +111,9 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
bool _showItemMenu = false;
int? _selectedToolbarItemIndex;
double previousKeyboardHeight = 0.0;
bool updateKeyboardHeight = true;
@override
void closeItemMenu() {
if (_showItemMenu) {
@ -123,7 +126,14 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
if (updateKeyboardHeight) {
previousKeyboardHeight = keyboardHeight;
}
// print('object $keyboardHeight');
return Column(
children: [
Container(
@ -147,15 +157,27 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
toolbarItems: widget.toolbarItems,
toolbarWidgetService: this,
itemWithMenuOnPressed: (selectedItemIndex) {
updateKeyboardHeight = false;
setState(() {
// If last selected item is selected again, toggle item menu
if (_selectedToolbarItemIndex == selectedItemIndex) {
_showItemMenu = !_showItemMenu;
if (!_showItemMenu) {
// updateKeyboardHeight = true;
widget.editorState.service.keyboardService
?.enableKeyBoard(widget.selection);
} else {
updateKeyboardHeight = false;
widget.editorState.service.keyboardService
?.closeKeyboard();
}
} else {
_selectedToolbarItemIndex = selectedItemIndex;
// If not, show item menu
_showItemMenu = true;
// close keyboard when menu pop up
widget.editorState.service.keyboardService
?.closeKeyboard();
}
@ -174,8 +196,8 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
onPressed: () {
setState(() {
_showItemMenu = false;
widget.editorState.service.keyboardService!
.enableKeyBoard(widget.selection);
widget.editorState.service.keyboardService
?.enableKeyBoard(widget.selection);
});
},
icon: const AFMobileIcon(
@ -187,14 +209,22 @@ class MobileToolbarWidgetState extends State<MobileToolbarWidget>
),
),
// only for MobileToolbarItem.withMenu
if (_showItemMenu && _selectedToolbarItemIndex != null)
MobileToolbarItemMenu(
editorState: widget.editorState,
itemMenuBuilder: () => widget
.toolbarItems[_selectedToolbarItemIndex!]
// pass current [MobileToolbarWidgetState] to be used to closeItemMenu
.itemMenuBuilder!(widget.editorState, widget.selection, this),
),
(_showItemMenu && _selectedToolbarItemIndex != null)
? Container(
constraints: BoxConstraints(
minHeight: previousKeyboardHeight,
),
child: MobileToolbarItemMenu(
editorState: widget.editorState,
itemMenuBuilder: () => widget
.toolbarItems[_selectedToolbarItemIndex!]
// pass current [MobileToolbarWidgetState] to be used to closeItemMenu
.itemMenuBuilder!(context, widget.editorState, this),
),
)
: SizedBox(
height: previousKeyboardHeight,
),
],
);
}
@ -239,9 +269,10 @@ class _ToolbarItemListView extends StatelessWidget {
return ListView.builder(
itemBuilder: (context, index) {
final toolbarItem = toolbarItems[index];
final icon = toolbarItem.itemIconBuilder(
final icon = toolbarItem.itemIconBuilder?.call(
context,
editorState,
toolbarWidgetService,
);
if (icon == null) {
return const SizedBox.shrink();
@ -256,8 +287,8 @@ class _ToolbarItemListView extends StatelessWidget {
// close menu if other item's menu is still on the screen
toolbarWidgetService.closeItemMenu();
toolbarItems[index].actionHandler?.call(
context,
editorState,
selection,
);
}
},

View file

@ -1,16 +1,16 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
typedef MobileToolbarItemMenuBuilder = Widget Function(
typedef MobileToolbarItemIconBuilder = Widget Function(
BuildContext context,
EditorState editorState,
Selection selection,
// To access to the state of MobileToolbarWidget
MobileToolbarWidgetService service,
);
typedef MobileToolbarItemActionHandler = void Function(
BuildContext context,
EditorState editorState,
Selection selection,
);
class MobileToolbarItem {
@ -19,20 +19,21 @@ class MobileToolbarItem {
required this.itemIconBuilder,
required this.actionHandler,
}) : hasMenu = false,
itemMenuBuilder = null;
itemMenuBuilder = null,
assert(itemIconBuilder != null && actionHandler != null);
/// Tool bar item that opens a menu to show options
const MobileToolbarItem.withMenu({
required this.itemIconBuilder,
required this.itemMenuBuilder,
}) : hasMenu = true,
actionHandler = null;
actionHandler = null,
assert(itemMenuBuilder != null && itemIconBuilder != null);
// if the result is null, the item will be hidden
final MobileToolbarItemIconBuilder? itemIconBuilder;
final MobileToolbarItemActionHandler? actionHandler;
final bool hasMenu;
final Widget? Function(
BuildContext context,
EditorState editorState,
) itemIconBuilder;
final MobileToolbarItemMenuBuilder? itemMenuBuilder;
final MobileToolbarItemActionHandler? actionHandler;
final MobileToolbarItemIconBuilder? itemMenuBuilder;
}

View file

@ -7,14 +7,35 @@ import 'package:flutter/material.dart';
/// itemHighlightColor -> selected item border color
///
/// itemOutlineColor -> item border color
class MobileToolbarStyle extends InheritedWidget {
class MobileToolbarTheme extends InheritedWidget {
const MobileToolbarTheme({
super.key,
this.backgroundColor = Colors.white,
this.foregroundColor = const Color(0xff676666),
this.clearDiagonalLineColor = const Color(0xffB3261E),
this.itemHighlightColor = const Color(0xff1F71AC),
this.itemOutlineColor = const Color(0xFFE3E3E3),
this.tabBarSelectedBackgroundColor = const Color(0x23808080),
this.tabBarSelectedForegroundColor = Colors.black,
this.primaryColor = const Color(0xff1F71AC),
this.onPrimaryColor = Colors.white,
this.outlineColor = const Color(0xFFE3E3E3),
this.toolbarHeight = 50.0,
this.borderRadius = 6.0,
this.buttonHeight = 40.0,
this.buttonSpacing = 8.0,
this.buttonBorderWidth = 1.0,
this.buttonSelectedBorderWidth = 2.0,
required super.child,
});
final Color backgroundColor;
final Color foregroundColor;
final Color clearDiagonalLineColor;
final Color itemHighlightColor;
final Color itemOutlineColor;
final Color tabbarSelectedBackgroundColor;
final Color tabbarSelectedForegroundColor;
final Color tabBarSelectedBackgroundColor;
final Color tabBarSelectedForegroundColor;
final Color primaryColor;
final Color onPrimaryColor;
final Color outlineColor;
@ -25,44 +46,25 @@ class MobileToolbarStyle extends InheritedWidget {
final double buttonBorderWidth;
final double buttonSelectedBorderWidth;
const MobileToolbarStyle({
super.key,
required this.backgroundColor,
required this.foregroundColor,
required this.clearDiagonalLineColor,
required this.itemHighlightColor,
required this.itemOutlineColor,
required this.tabbarSelectedBackgroundColor,
required this.tabbarSelectedForegroundColor,
required this.primaryColor,
required this.onPrimaryColor,
required this.outlineColor,
required this.toolbarHeight,
required this.borderRadius,
required this.buttonHeight,
required this.buttonSpacing,
required this.buttonBorderWidth,
required this.buttonSelectedBorderWidth,
required super.child,
});
static MobileToolbarTheme of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MobileToolbarTheme>()!;
}
static MobileToolbarStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MobileToolbarStyle>()!;
static MobileToolbarTheme? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MobileToolbarTheme>();
}
@override
bool updateShouldNotify(covariant MobileToolbarStyle oldWidget) {
// This function is called whenever the inherited widget is rebuilt.
// It should return true if the new widget's values are different from the old widget's values.
bool updateShouldNotify(covariant MobileToolbarTheme oldWidget) {
return backgroundColor != oldWidget.backgroundColor ||
foregroundColor != oldWidget.foregroundColor ||
clearDiagonalLineColor != oldWidget.clearDiagonalLineColor ||
itemHighlightColor != oldWidget.itemHighlightColor ||
itemOutlineColor != oldWidget.itemOutlineColor ||
tabbarSelectedBackgroundColor !=
oldWidget.tabbarSelectedBackgroundColor ||
tabbarSelectedForegroundColor !=
oldWidget.tabbarSelectedForegroundColor ||
tabBarSelectedBackgroundColor !=
oldWidget.tabBarSelectedBackgroundColor ||
tabBarSelectedForegroundColor !=
oldWidget.tabBarSelectedForegroundColor ||
primaryColor != oldWidget.primaryColor ||
onPrimaryColor != oldWidget.onPrimaryColor ||
outlineColor != oldWidget.outlineColor ||

View file

@ -0,0 +1,342 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
class MobileToolbarV2 extends StatelessWidget {
const MobileToolbarV2({
super.key,
required this.editorState,
required this.toolbarItems,
});
final EditorState editorState;
final List<MobileToolbarItem> toolbarItems;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Selection?>(
valueListenable: editorState.selectionNotifier,
builder: (_, Selection? selection, child) {
// if the selection is null, hide the toolbar
if (selection == null) {
return const SizedBox.shrink();
}
// if the MobileToolbarTheme is not provided, provide it
if (MobileToolbarTheme.maybeOf(context) == null) {
child = MobileToolbarTheme(
child: child!,
);
}
return RepaintBoundary(
child: child,
);
},
child: _MobileToolbar(
editorState: editorState,
toolbarItems: toolbarItems,
),
);
}
}
class _MobileToolbar extends StatefulWidget {
const _MobileToolbar({
required this.editorState,
required this.toolbarItems,
});
final EditorState editorState;
final List<MobileToolbarItem> toolbarItems;
@override
State<_MobileToolbar> createState() => _MobileToolbarState();
}
class _MobileToolbarState extends State<_MobileToolbar>
implements MobileToolbarWidgetService {
// used to control the toolbar menu items
PropertyValueNotifier<bool> showMenuNotifier = PropertyValueNotifier(false);
// when the users click the menu item, the keyboard will be hidden,
// but in this case, we don't want to update the cached keyboard height.
// This is because we want to keep the same height when the menu is shown.
bool canUpdateCachedKeyboardHeight = true;
ValueNotifier<double> cachedKeyboardHeight = ValueNotifier(0.0);
double get keyboardHeight => MediaQuery.of(context).viewInsets.bottom;
// used to check if click the same item again
int? selectedMenuIndex;
Selection? currentSelection;
@override
void initState() {
super.initState();
currentSelection = widget.editorState.selection;
}
@override
void didUpdateWidget(covariant _MobileToolbar oldWidget) {
super.didUpdateWidget(oldWidget);
if (currentSelection != widget.editorState.selection) {
currentSelection = widget.editorState.selection;
closeItemMenu();
}
}
@override
void dispose() {
showMenuNotifier.dispose();
cachedKeyboardHeight.dispose();
super.dispose();
}
@override
void reassemble() {
super.reassemble();
canUpdateCachedKeyboardHeight = true;
closeItemMenu();
_closeKeyboard();
}
@override
Widget build(BuildContext context) {
// update the keyboard height here.
// try to get the height in `didChangeMetrics`, but it's not accurate.
if (canUpdateCachedKeyboardHeight) {
cachedKeyboardHeight.value = keyboardHeight;
}
// toolbar
// - if the menu is shown, the toolbar will be pushed up by the height of the menu
// - otherwise, add a spacer to push the toolbar up when the keyboard is shown
return Column(
children: [
_buildToolbar(context),
_buildMenuOrSpacer(context),
],
);
}
@override
void closeItemMenu() {
showMenuNotifier.value = false;
}
void showItemMenu() {
showMenuNotifier.value = true;
}
// toolbar list view and close keyboard/menu button
Widget _buildToolbar(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final style = MobileToolbarTheme.of(context);
return Container(
width: width,
height: style.toolbarHeight,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: style.itemOutlineColor,
),
bottom: BorderSide(color: style.itemOutlineColor),
),
color: style.backgroundColor,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// toolbar list view
Expanded(
child: _ToolbarItemListView(
toolbarItems: widget.toolbarItems,
editorState: widget.editorState,
toolbarWidgetService: this,
itemWithActionOnPressed: (_) {
if (showMenuNotifier.value) {
closeItemMenu();
_showKeyboard();
// update the cached keyboard height after the keyboard is shown
Debounce.debounce('canUpdateCachedKeyboardHeight',
const Duration(milliseconds: 500), () {
canUpdateCachedKeyboardHeight = true;
});
}
},
itemWithMenuOnPressed: (index) {
// click the same one
if (selectedMenuIndex == index && showMenuNotifier.value) {
// if the menu is shown, close it and show the keyboard
closeItemMenu();
_showKeyboard();
// update the cached keyboard height after the keyboard is shown
Debounce.debounce('canUpdateCachedKeyboardHeight',
const Duration(milliseconds: 500), () {
canUpdateCachedKeyboardHeight = true;
});
} else {
canUpdateCachedKeyboardHeight = false;
selectedMenuIndex = index;
showItemMenu();
_closeKeyboard();
}
},
),
),
// divider
const Padding(
padding: EdgeInsets.symmetric(
vertical: 8,
),
child: VerticalDivider(
width: 1,
),
),
// close menu or close keyboard button
ValueListenableBuilder(
valueListenable: showMenuNotifier,
builder: (_, showingMenu, __) {
return _CloseKeyboardOrMenuButton(
showingMenu: showingMenu,
onPressed: () {
if (showingMenu) {
// close the menu and show the keyboard
closeItemMenu();
_showKeyboard();
} else {
// close the keyboard and clear the selection
// if the selection is null, the keyboard and the toolbar will be hidden automatically
widget.editorState.selection = null;
}
},
);
},
),
const SizedBox(
width: 4.0,
),
],
),
);
}
// if there's no menu, we need to add a spacer to push the toolbar up when the keyboard is shown
Widget _buildMenuOrSpacer(BuildContext context) {
return ValueListenableBuilder(
valueListenable: cachedKeyboardHeight,
builder: (_, height, ___) {
return ValueListenableBuilder(
valueListenable: showMenuNotifier,
builder: (_, showingMenu, __) {
return ConstrainedBox(
constraints: BoxConstraints(minHeight: height),
child: (showingMenu && selectedMenuIndex != null)
? MobileToolbarItemMenu(
editorState: widget.editorState,
itemMenuBuilder: () => widget
.toolbarItems[selectedMenuIndex!].itemMenuBuilder!
.call(
context,
widget.editorState,
this,
),
)
: const SizedBox.shrink(),
);
},
);
},
);
}
void _showKeyboard() {
final selection = widget.editorState.selection;
if (selection != null) {
widget.editorState.service.keyboardService?.enableKeyBoard(selection);
}
}
void _closeKeyboard() {
widget.editorState.service.keyboardService?.closeKeyboard();
}
}
class _ToolbarItemListView extends StatelessWidget {
const _ToolbarItemListView({
required this.toolbarItems,
required this.editorState,
required this.toolbarWidgetService,
required this.itemWithMenuOnPressed,
required this.itemWithActionOnPressed,
});
final Function(int index) itemWithMenuOnPressed;
final Function(int index) itemWithActionOnPressed;
final List<MobileToolbarItem> toolbarItems;
final EditorState editorState;
final MobileToolbarWidgetService toolbarWidgetService;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
final toolbarItem = toolbarItems[index];
final icon = toolbarItem.itemIconBuilder?.call(
context,
editorState,
toolbarWidgetService,
);
if (icon == null) {
return const SizedBox.shrink();
}
return IconButton(
icon: icon,
onPressed: () {
if (toolbarItem.hasMenu) {
// open /close current item menu through its parent widget(MobileToolbarWidget)
itemWithMenuOnPressed(index);
} else {
itemWithActionOnPressed(index);
// close menu if other item's menu is still on the screen
toolbarWidgetService.closeItemMenu();
toolbarItems[index].actionHandler?.call(
context,
editorState,
);
}
},
);
},
itemCount: toolbarItems.length,
scrollDirection: Axis.horizontal,
);
}
}
class _CloseKeyboardOrMenuButton extends StatelessWidget {
const _CloseKeyboardOrMenuButton({
required this.showingMenu,
required this.onPressed,
});
final bool showingMenu;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return IconButton(
padding: EdgeInsets.zero,
onPressed: onPressed,
icon: showingMenu
? const AFMobileIcon(
afMobileIcons: AFMobileIcons.close,
)
: const Icon(Icons.keyboard_hide),
);
}
}

View file

@ -0,0 +1,137 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final blocksMobileToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.list,
),
itemMenuBuilder: (_, editorState, __) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
return _BlocksMenu(editorState, selection);
},
);
class _BlocksMenu extends StatefulWidget {
const _BlocksMenu(
this.editorState,
this.selection,
);
final Selection selection;
final EditorState editorState;
@override
State<_BlocksMenu> createState() => _BlocksMenuState();
}
class _BlocksMenuState extends State<_BlocksMenu> {
final lists = [
// heading
_ListUnit(
icon: AFMobileIcons.h1,
label: AppFlowyEditorL10n.current.mobileHeading1,
name: HeadingBlockKeys.type,
level: 1,
),
_ListUnit(
icon: AFMobileIcons.h2,
label: AppFlowyEditorL10n.current.mobileHeading2,
name: HeadingBlockKeys.type,
level: 2,
),
_ListUnit(
icon: AFMobileIcons.h3,
label: AppFlowyEditorL10n.current.mobileHeading3,
name: HeadingBlockKeys.type,
level: 3,
),
// list
_ListUnit(
icon: AFMobileIcons.bulletedList,
label: AppFlowyEditorL10n.current.bulletedList,
name: BulletedListBlockKeys.type,
),
_ListUnit(
icon: AFMobileIcons.numberedList,
label: AppFlowyEditorL10n.current.numberedList,
name: NumberedListBlockKeys.type,
),
_ListUnit(
icon: AFMobileIcons.checkbox,
label: AppFlowyEditorL10n.current.checkbox,
name: TodoListBlockKeys.type,
),
_ListUnit(
icon: AFMobileIcons.quote,
label: AppFlowyEditorL10n.current.quote,
name: QuoteBlockKeys.type,
),
];
@override
Widget build(BuildContext context) {
final children = lists.map((list) {
// Check if current node is list and its type
final node = widget.editorState.getNodeAtPath(
widget.selection.start.path,
)!;
final isSelected = node.type == list.name &&
(list.level == null ||
node.attributes[HeadingBlockKeys.level] == list.level);
return MobileToolbarItemMenuBtn(
icon: AFMobileIcon(afMobileIcons: list.icon),
label: Text(list.label),
isSelected: isSelected,
onPressed: () {
setState(() {
widget.editorState.formatNode(
widget.selection,
(node) => node.copyWith(
type: isSelected ? ParagraphBlockKeys.type : list.name,
attributes: {
ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(),
blockComponentBackgroundColor:
node.attributes[blockComponentBackgroundColor],
if (!isSelected && list.name == TodoListBlockKeys.type)
TodoListBlockKeys.checked: false,
if (!isSelected && list.name == HeadingBlockKeys.type)
HeadingBlockKeys.level: list.level,
},
),
);
});
},
);
}).toList();
return GridView(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 5,
),
children: children,
);
}
}
class _ListUnit {
final AFMobileIcons icon;
final String label;
final String name;
final int? level;
_ListUnit({
required this.icon,
required this.label,
required this.name,
this.level,
});
}

View file

@ -1,8 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
final codeMobileToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, __) =>
const AFMobileIcon(afMobileIcons: AFMobileIcons.code),
actionHandler: (editorState, selection) =>
editorState.toggleAttribute(AppFlowyRichTextKeys.code),
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.code,
),
actionHandler: (_, editorState) => editorState.toggleAttribute(
AppFlowyRichTextKeys.code,
),
);

View file

@ -22,7 +22,7 @@ class _BackgroundColorOptionsWidgetsState
extends State<BackgroundColorOptionsWidgets> {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
final colorOptions =
widget.backgroundColorOptions ?? generateHighlightColorOptions();
final selection = widget.selection;

View file

@ -6,9 +6,14 @@ MobileToolbarItem buildTextAndBackgroundColorMobileToolbarItem({
List<ColorOption>? backgroundColorOptions,
}) {
return MobileToolbarItem.withMenu(
itemIconBuilder: (_, __) =>
const AFMobileIcon(afMobileIcons: AFMobileIcons.color),
itemMenuBuilder: (editorState, selection, _) {
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.color,
),
itemMenuBuilder: (_, editorState, ___) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
return _TextAndBackgroundColorMenu(
editorState,
selection,
@ -41,7 +46,7 @@ class _TextAndBackgroundColorMenuState
extends State<_TextAndBackgroundColorMenu> {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
List<Tab> myTabs = <Tab>[
Tab(
text: AppFlowyEditorL10n.current.textColor,
@ -58,10 +63,10 @@ class _TextAndBackgroundColorMenuState
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
tabs: myTabs,
labelColor: style.tabbarSelectedForegroundColor,
labelColor: style.tabBarSelectedForegroundColor,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(style.borderRadius),
color: style.tabbarSelectedBackgroundColor,
color: style.tabBarSelectedForegroundColor,
),
// remove the bottom line of TabBar
dividerColor: Colors.transparent,

View file

@ -21,7 +21,7 @@ class TextColorOptionsWidgets extends StatefulWidget {
class _TextColorOptionsWidgetsState extends State<TextColorOptionsWidgets> {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
final selection = widget.selection;
final nodes = widget.editorState.getNodesInSelection(selection);

View file

@ -12,7 +12,7 @@ class ClearColorButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
return InkWell(
onTap: onPressed,

View file

@ -17,7 +17,7 @@ class ColorButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
return InkWell(
onTap: onPressed,
child: Container(

View file

@ -1,9 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
final dividerMobileToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, __) =>
const AFMobileIcon(afMobileIcons: AFMobileIcons.divider),
actionHandler: ((editorState, selection) {
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.divider,
),
actionHandler: (_, editorState) {
// same as the [handler] of [dividerMenuItem] in Desktop
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
@ -29,5 +30,5 @@ final dividerMobileToolbarItem = MobileToolbarItem.action(
transaction.afterSelection =
Selection.collapsed(Position(path: insertedPath.next));
editorState.apply(transaction);
}),
},
);

View file

@ -2,9 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final headingMobileToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, __) =>
const AFMobileIcon(afMobileIcons: AFMobileIcons.heading),
itemMenuBuilder: (editorState, selection, _) {
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.heading,
),
itemMenuBuilder: (_, editorState, __) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
return _HeadingMenu(
selection,
editorState,
@ -46,7 +51,7 @@ class _HeadingMenuState extends State<_HeadingMenu> {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
final size = MediaQuery.sizeOf(context);
final btnList = headings.map((currentHeading) {
// Check if current node is heading and its level

View file

@ -2,10 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final linkMobileToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, __) => const AFMobileIcon(
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.link,
),
itemMenuBuilder: (editorState, selection, itemMenuService) {
itemMenuBuilder: (_, editorState, itemMenuService) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
final String? linkText = editorState.getDeltaAttributeValueInSelection(
AppFlowyRichTextKeys.href,
selection,
@ -65,7 +69,7 @@ class _MobileLinkMenuState extends State<MobileLinkMenu> {
@override
Widget build(BuildContext context) {
final style = MobileToolbarStyle.of(context);
final style = MobileToolbarTheme.of(context);
const double spacing = 8;
return Material(
// TextField widget needs to be wrapped in a Material widget to provide a visual appearance

View file

@ -2,9 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final listMobileToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, __) =>
const AFMobileIcon(afMobileIcons: AFMobileIcons.list),
itemMenuBuilder: (editorState, selection, _) {
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.list,
),
itemMenuBuilder: (_, editorState, __) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
return _ListMenu(editorState, selection);
},
);

View file

@ -1,10 +1,18 @@
import 'package:appflowy_editor/appflowy_editor.dart';
final quoteMobileToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, __) =>
const AFMobileIcon(afMobileIcons: AFMobileIcons.quote),
actionHandler: ((editorState, selection) {
final node = editorState.getNodeAtPath(selection.start.path)!;
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
afMobileIcons: AFMobileIcons.quote,
),
actionHandler: (context, editorState) async {
final selection = editorState.selection;
if (selection == null) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return;
}
final isQuote = node.type == QuoteBlockKeys.type;
editorState.formatNode(
selection,
@ -15,5 +23,5 @@ final quoteMobileToolbarItem = MobileToolbarItem.action(
},
),
);
}),
},
);

Some files were not shown because too many files have changed in this diff Show more