fix: toolbar launch review issues (#7631)

* fix: keep the turn into menu within six-dot same as toolbar

* fix: change some icon color within toolbar

* fix: improve toolbar

* chore: update editor dependency

* fix: update editor dependency
This commit is contained in:
Morn 2025-03-27 14:19:51 +08:00 committed by GitHub
parent 76cb23e233
commit a26ebbccc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 714 additions and 409 deletions

View File

@ -22,6 +22,7 @@ import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys;
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -350,6 +351,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
final isViewDeleted = context.read<DocumentBloc>().state.isDeleted;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final themeV2 = AFThemeExtensionV2.of(context);
final editor = Directionality(
textDirection: textDirection,
child: AppFlowyEditor(
@ -428,7 +431,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
),
);
}
return Center(
child: FloatingToolbar(
floatingToolbarHeight: 40,
@ -436,12 +438,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
style: FloatingToolbarStyle(
backgroundColor: Theme.of(context).cardColor,
toolbarActiveColor: Color(0xffe0f8fd),
toolbarElevation: 10,
),
items: toolbarItems,
decoration: ShapeDecoration(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
boxShadow: [
BoxShadow(
offset: Offset(0, 4),
blurRadius: 24,
color: themeV2.shadow_medium,
),
],
),
toolbarBuilder: (context, child, onDismiss, isMetricsChanged) =>
DesktopFloatingToolbar(

View File

@ -2,8 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockKeys, quoteNode;
@ -149,214 +148,134 @@ class TurnIntoOptionMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (hasNonSupportedTypes) {
return buildItem(
pateItem,
textSuggestionItem,
context.read<BlockActionOptionCubit>().editorState,
);
}
return _buildTurnIntoOptions(context, node);
}
Widget _buildTurnIntoOptions(BuildContext context, Node node) {
final editorState = context.read<BlockActionOptionCubit>().editorState;
SuggestionItem currentSuggestionItem = textSuggestionItem;
final List<SuggestionItem> suggestionItems = suggestions.sublist(0, 4);
final List<SuggestionItem> turnIntoItems =
suggestions.sublist(4, suggestions.length);
final textColor = Color(0xff99A1A8);
void refreshSuggestions() {
final selection = editorState.selection;
if (selection == null || !selection.isSingle) return;
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null || node.delta == null) return;
final nodeType = node.type;
SuggestionType? suggestionType;
if (nodeType == HeadingBlockKeys.type) {
final level = node.attributes[HeadingBlockKeys.level] ?? 1;
if (level == 1) {
suggestionType = SuggestionType.h1;
} else if (level == 2) {
suggestionType = SuggestionType.h2;
} else if (level == 3) {
suggestionType = SuggestionType.h3;
}
} else if (nodeType == ToggleListBlockKeys.type) {
final level = node.attributes[ToggleListBlockKeys.level];
if (level == null) {
suggestionType = SuggestionType.toggle;
} else if (level == 1) {
suggestionType = SuggestionType.toggleH1;
} else if (level == 2) {
suggestionType = SuggestionType.toggleH2;
} else if (level == 3) {
suggestionType = SuggestionType.toggleH3;
}
} else {
suggestionType = nodeType2SuggestionType[nodeType];
}
if (suggestionType == null) return;
suggestionItems.clear();
turnIntoItems.clear();
for (final item in suggestions) {
if (item.type.group == suggestionType.group &&
item.type != suggestionType) {
suggestionItems.add(item);
} else {
turnIntoItems.add(item);
}
}
currentSuggestionItem =
suggestions.where((item) => item.type == suggestionType).first;
}
refreshSuggestions();
return Column(
mainAxisSize: MainAxisSize.min,
children: _buildTurnIntoOptions(context, node),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildSubTitle(
LocaleKeys.document_toolbar_suggestions.tr(),
textColor,
),
...List.generate(suggestionItems.length, (index) {
return buildItem(
suggestionItems[index],
currentSuggestionItem,
editorState,
);
}),
buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor),
...List.generate(turnIntoItems.length, (index) {
return buildItem(
turnIntoItems[index],
currentSuggestionItem,
editorState,
);
}),
],
);
}
List<Widget> _buildTurnIntoOptions(BuildContext context, Node node) {
final children = <Widget>[];
if (hasNonSupportedTypes) {
return children
..add(
_TurnInfoButton(
type: SubPageBlockKeys.type,
node: node,
),
);
}
for (final type in EditorOptionActionType.turnInto.supportTypes) {
if (type == ToggleListBlockKeys.type) {
// toggle list block and toggle heading block are the same type,
// but they have different attributes.
// toggle list block
children.add(
_TurnInfoButton(
type: type,
node: node,
),
);
// toggle heading block
for (final i in [1, 2, 3]) {
children.add(
_TurnInfoButton(
type: type,
node: node,
level: i,
),
);
}
} else if (type != HeadingBlockKeys.type) {
children.add(
_TurnInfoButton(
type: type,
node: node,
),
);
} else {
for (final i in [1, 2, 3]) {
children.add(
_TurnInfoButton(
type: type,
node: node,
level: i,
),
);
}
}
}
return children;
}
}
class _TurnInfoButton extends StatelessWidget {
const _TurnInfoButton({
required this.type,
required this.node,
this.level,
});
final String type;
final Node node;
final int? level;
@override
Widget build(BuildContext context) {
final name = _buildLocalization(type, level: level);
final leftIcon = _buildLeftIcon(type, level: level);
final rightIcon = _buildRightIcon(type, node, level: level);
return HoverButton(
name: name,
leftIcon: FlowySvg(leftIcon),
rightIcon: rightIcon,
itemHeight: ActionListSizes.itemHeight,
onTap: () => BlockActionOptionCubit.turnIntoBlock(
type,
node,
context.read<BlockActionOptionCubit>().editorState,
level: level,
currentViewId: getIt<MenuSharedState>().latestOpenView?.id,
Widget buildSubTitle(String text, Color color) {
return Container(
height: 32,
margin: EdgeInsets.symmetric(horizontal: 8),
child: Align(
alignment: Alignment.centerLeft,
child: FlowyText.semibold(
text,
color: color,
figmaLineHeight: 16,
),
),
);
}
Widget? _buildRightIcon(String type, Node node, {int? level}) {
if (type != node.type) {
return null;
}
if (node.type == HeadingBlockKeys.type) {
final nodeLevel = node.attributes[HeadingBlockKeys.level] ?? 1;
if (level != nodeLevel) {
return null;
}
}
if (node.type == ToggleListBlockKeys.type) {
final nodeLevel = node.attributes[ToggleListBlockKeys.level];
if (level != nodeLevel) {
return null;
}
}
return const FlowySvg(
FlowySvgs.workspace_selected_s,
blendMode: null,
Widget buildItem(
SuggestionItem item,
SuggestionItem currentSuggestionItem,
EditorState state,
) {
final isSelected = item.type == currentSuggestionItem.type;
return SizedBox(
height: 36,
child: FlowyButton(
leftIconSize: const Size.square(20),
leftIcon: FlowySvg(item.svg),
iconPadding: 12,
text: FlowyText(
item.title,
fontWeight: FontWeight.w400,
figmaLineHeight: 20,
),
rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null,
onTap: () => item.onTap.call(state, false),
),
);
}
FlowySvgData _buildLeftIcon(String type, {int? level}) {
if (type == ParagraphBlockKeys.type) {
return FlowySvgs.type_text_m;
} else if (type == HeadingBlockKeys.type) {
switch (level) {
case 1:
return FlowySvgs.type_h1_m;
case 2:
return FlowySvgs.type_h2_m;
case 3:
return FlowySvgs.type_h3_m;
default:
return FlowySvgs.type_text_m;
}
} else if (type == QuoteBlockKeys.type) {
return FlowySvgs.type_quote_m;
} else if (type == BulletedListBlockKeys.type) {
return FlowySvgs.type_bulleted_list_m;
} else if (type == NumberedListBlockKeys.type) {
return FlowySvgs.type_numbered_list_m;
} else if (type == TodoListBlockKeys.type) {
return FlowySvgs.type_todo_m;
} else if (type == CalloutBlockKeys.type) {
return FlowySvgs.type_callout_m;
} else if (type == SubPageBlockKeys.type) {
return FlowySvgs.icon_document_s;
} else if (type == ToggleListBlockKeys.type) {
switch (level) {
case 1:
return FlowySvgs.type_toggle_h1_m;
case 2:
return FlowySvgs.type_toggle_h2_m;
case 3:
return FlowySvgs.type_toggle_h3_m;
default:
return FlowySvgs.type_toggle_list_m;
}
}
throw UnimplementedError('Unsupported block type: $type');
}
String _buildLocalization(
String type, {
int? level,
}) {
switch (type) {
case ParagraphBlockKeys.type:
return LocaleKeys.document_slashMenu_name_text.tr();
case HeadingBlockKeys.type:
switch (level) {
case 1:
return LocaleKeys.document_slashMenu_name_heading1.tr();
case 2:
return LocaleKeys.document_slashMenu_name_heading2.tr();
case 3:
return LocaleKeys.document_slashMenu_name_heading3.tr();
default:
return LocaleKeys.document_slashMenu_name_text.tr();
}
case QuoteBlockKeys.type:
return LocaleKeys.document_slashMenu_name_quote.tr();
case BulletedListBlockKeys.type:
return LocaleKeys.editor_bulletedListShortForm.tr();
case NumberedListBlockKeys.type:
return LocaleKeys.editor_numberedListShortForm.tr();
case TodoListBlockKeys.type:
return LocaleKeys.editor_checkbox.tr();
case CalloutBlockKeys.type:
return LocaleKeys.document_slashMenu_name_callout.tr();
case SubPageBlockKeys.type:
return LocaleKeys.editor_page.tr();
case ToggleListBlockKeys.type:
switch (level) {
case 1:
return LocaleKeys.editor_toggleHeading1ShortForm.tr();
case 2:
return LocaleKeys.editor_toggleHeading2ShortForm.tr();
case 3:
return LocaleKeys.editor_toggleHeading3ShortForm.tr();
default:
return LocaleKeys.editor_toggleListShortForm.tr();
}
}
throw UnimplementedError('Unsupported block type: $type');
}
}

View File

@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -118,7 +119,7 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
}
Widget buildChild(BuildContext context) {
final iconColor = Theme.of(context).iconTheme.color;
final themeV2 = AFThemeExtensionV2.of(context);
final child = FlowyIconButton(
width: 48,
height: 32,
@ -130,13 +131,13 @@ class _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {
FlowySvg(
FlowySvgs.toolbar_ai_writer_m,
size: Size.square(20),
color: iconColor,
color: themeV2.icon_primary,
),
HSpace(4),
FlowySvg(
FlowySvgs.toolbar_arrow_down_m,
size: Size(12, 20),
color: iconColor,
color: themeV2.icon_tertiary,
),
],
),
@ -187,7 +188,7 @@ class ImproveWritingButton extends StatelessWidget {
icon: FlowySvg(
FlowySvgs.toolbar_ai_improve_writing_m,
size: Size.square(20.0),
color: Theme.of(context).iconTheme.color,
color: AFThemeExtensionV2.of(context).icon_primary,
),
onPressed: () {
if (_isAIEnabled(editorState)) {

View File

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';
@ -35,9 +37,7 @@ bool onlyShowInSingleTextTypeSelectionAndExcludeTable(
notShowInTable(editorState);
}
bool enableSuggestions(
EditorState editorState,
) {
bool enableSuggestions(EditorState editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isSingle) {
return false;
@ -46,10 +46,18 @@ bool enableSuggestions(
if (node == null) {
return false;
}
if (isNarrowWindow(editorState)) return false;
return (node.delta != null && suggestionsItemTypes.contains(node.type)) &&
notShowInTable(editorState);
}
bool isNarrowWindow(EditorState editorState) {
final editorSize = editorState.renderBox?.size ?? Size.zero;
if (editorSize.width < 650) return true;
return false;
}
final Set<String> suggestionsItemTypes = {
...toolbarItemWhiteList,
ToggleListBlockKeys.type,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -61,10 +62,11 @@ class _DesktopFloatingToolbarState extends State<DesktopFloatingToolbar> {
) {
const toolbarHeight = 40, topLimit = toolbarHeight + 8;
final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState);
final menuWidth = isLongMenu ? 650.0 : 420.0;
final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final editorSize = editorState.renderBox?.size ?? Size.zero;
final menuWidth =
isLongMenu ? (isNarrowWindow(editorState) ? 490.0 : 660.0) : 420.0;
final editorRect = editorOffset & editorSize;
final left = rect.left, leftStart = 50;
final top =

View File

@ -148,6 +148,115 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
}
}
void showLinkCreateMenu(
BuildContext context,
EditorState editorState,
Selection selection,
) {
final (left, top, right, bottom, alignment) = _getPosition(editorState);
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
OverlayEntry? overlay;
void dismissOverlay() {
keepEditorFocusNotifier.decrease();
overlay?.remove();
overlay = null;
}
keepEditorFocusNotifier.increase();
overlay = FullScreenOverlayEntry(
top: top,
bottom: bottom,
left: left,
right: right,
dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) {
return LinkCreateMenu(
alignment: alignment,
editorState: editorState,
onSubmitted: (link, isPage) async {
await editorState.formatDelta(selection, {
BuiltInAttributeKey.href: link,
kIsPageLink: isPage,
});
dismissOverlay();
},
onDismiss: dismissOverlay,
);
},
).build();
Overlay.of(context, rootOverlay: true).insert(overlay!);
}
// get a proper position for link menu
(
double? left,
double? top,
double? right,
double? bottom,
LinkMenuAlignment alignment,
) _getPosition(
EditorState editorState,
) {
final rect = editorState.selectionRects().first;
const menuHeight = 222.0, menuWidth = 320.0;
double? left, right, top, bottom;
LinkMenuAlignment alignment = LinkMenuAlignment.topLeft;
final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero),
editorSize = editorState.renderBox!.size;
final editorBottom = editorSize.height + editorOffset.dy,
editorRight = editorSize.width + editorOffset.dx;
final overflowBottom = rect.bottom + menuHeight > editorBottom,
overflowTop = rect.top - menuHeight < 0,
overflowLeft = rect.left - menuWidth < 0,
overflowRight = rect.right + menuWidth > editorRight;
if (overflowTop && !overflowBottom) {
/// show at bottom
top = rect.bottom;
} else if (overflowBottom && !overflowTop) {
/// show at top
bottom = editorBottom - rect.top;
} else if (!overflowTop && !overflowBottom) {
/// show at bottom
top = rect.bottom;
} else {
top = 0;
}
if (overflowLeft && !overflowRight) {
/// show at right
left = rect.left;
} else if (overflowRight && !overflowLeft) {
/// show at left
right = editorRight - rect.right;
} else if (!overflowLeft && !overflowRight) {
/// show at right
left = rect.left;
} else {
left = 0;
}
if (left != null && top != null) {
alignment = LinkMenuAlignment.bottomRight;
} else if (left != null && bottom != null) {
alignment = LinkMenuAlignment.topRight;
} else if (right != null && top != null) {
alignment = LinkMenuAlignment.bottomLeft;
} else if (right != null && bottom != null) {
alignment = LinkMenuAlignment.topLeft;
}
return (left, top, right, bottom, alignment);
}
ShapeDecoration buildToolbarLinkDecoration(BuildContext context) =>
ShapeDecoration(
color: Theme.of(context).cardColor,

View File

@ -57,10 +57,19 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
Attributes get attribute => widget.attribute;
HoverTriggerKey get triggerKey => HoverTriggerKey(widget.node.id, selection);
@override
void initState() {
super.initState();
getIt<LinkHoverTriggers>()._add(triggerKey, showLinkHoverMenu);
}
@override
void dispose() {
hoverMenuController.close();
editMenuController.close();
getIt<LinkHoverTriggers>()._remove(triggerKey, showLinkHoverMenu);
super.dispose();
}
@ -217,6 +226,31 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
.setData(ClipboardServiceData(plainText: href));
hoverMenuController.close();
}
void removeLink(
EditorState editorState,
Selection selection,
bool isHref,
) {
if (!isHref) return;
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final index = selection.normalized.startIndex;
final length = selection.length;
final transaction = editorState.transaction
..formatText(
node,
index,
length,
{
BuiltInAttributeKey.href: null,
kIsPageLink: null,
},
);
editorState.apply(transaction);
}
}
class LinkHoverMenu extends StatelessWidget {
@ -320,3 +354,44 @@ class LinkHoverMenu extends StatelessWidget {
);
}
}
class HoverTriggerKey {
HoverTriggerKey(this.nodeId, this.selection);
final String nodeId;
final Selection selection;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HoverTriggerKey &&
runtimeType == other.runtimeType &&
nodeId == other.nodeId &&
selection == other.selection;
@override
int get hashCode => nodeId.hashCode ^ selection.hashCode;
}
class LinkHoverTriggers {
final Map<HoverTriggerKey, Set<VoidCallback>> _map = {};
void _add(HoverTriggerKey key, VoidCallback callback) {
final callbacks = _map[key] ?? {};
callbacks.add(callback);
_map[key] = callbacks;
}
void _remove(HoverTriggerKey key, VoidCallback callback) {
final callbacks = _map[key] ?? {};
callbacks.remove(callback);
_map[key] = callbacks;
}
void call(HoverTriggerKey key) {
final callbacks = _map[key] ?? {};
for (final callback in callbacks) {
callback.call();
}
}
}

View File

@ -6,6 +6,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
@ -79,7 +80,7 @@ class _FormatToolbarItem extends ToolbarItem {
size: Size.square(20.0),
color: (isDark && isHighlight)
? Color(0xFF282E3A)
: Theme.of(context).iconTheme.color,
: AFThemeExtensionV2.of(context).icon_primary,
),
onPressed: () => editorState.toggleAttribute(
name,

View File

@ -1,7 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
@ -14,7 +17,8 @@ const kIsPageLink = 'is_page_link';
final customLinkItem = ToolbarItem(
id: ToolbarId.link.id,
group: 4,
isActive: onlyShowInSingleSelectionAndTextType,
isActive: (state) =>
!isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state),
builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) {
final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection);
@ -42,9 +46,11 @@ final customLinkItem = ToolbarItem(
onPressed: () {
toolbarCubit?.dismiss();
if (isHref) {
removeLink(editorState, selection, isHref);
getIt<LinkHoverTriggers>().call(
HoverTriggerKey(nodes.first.id, selection),
);
} else {
_showLinkMenu(context, editorState, selection, isHref);
showLinkCreateMenu(context, editorState, selection);
}
},
);
@ -62,78 +68,6 @@ final customLinkItem = ToolbarItem(
},
);
void removeLink(
EditorState editorState,
Selection selection,
bool isHref,
) {
if (!isHref) return;
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final index = selection.normalized.startIndex;
final length = selection.length;
final transaction = editorState.transaction
..formatText(
node,
index,
length,
{
BuiltInAttributeKey.href: null,
kIsPageLink: null,
},
);
editorState.apply(transaction);
}
void _showLinkMenu(
BuildContext context,
EditorState editorState,
Selection selection,
bool isHref,
) {
final (left, top, right, bottom, alignment) = _getPosition(editorState);
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
OverlayEntry? overlay;
void dismissOverlay() {
keepEditorFocusNotifier.decrease();
overlay?.remove();
overlay = null;
}
keepEditorFocusNotifier.increase();
overlay = FullScreenOverlayEntry(
top: top,
bottom: bottom,
left: left,
right: right,
dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) {
return LinkCreateMenu(
alignment: alignment,
editorState: editorState,
onSubmitted: (link, isPage) async {
await editorState.formatDelta(selection, {
BuiltInAttributeKey.href: link,
kIsPageLink: isPage,
});
dismissOverlay();
},
onDismiss: dismissOverlay,
);
},
).build();
Overlay.of(context, rootOverlay: true).insert(overlay!);
}
extension AttributeExtension on Attributes {
bool get isPage {
if (this[kIsPageLink] is bool) {
@ -143,69 +77,6 @@ extension AttributeExtension on Attributes {
}
}
// get a proper position for link menu
(
double? left,
double? top,
double? right,
double? bottom,
LinkMenuAlignment alignment,
) _getPosition(
EditorState editorState,
) {
final rect = editorState.selectionRects().first;
const menuHeight = 222.0, menuWidth = 320.0;
double? left, right, top, bottom;
LinkMenuAlignment alignment = LinkMenuAlignment.topLeft;
final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero),
editorSize = editorState.renderBox!.size;
final editorBottom = editorSize.height + editorOffset.dy,
editorRight = editorSize.width + editorOffset.dx;
final overflowBottom = rect.bottom + menuHeight > editorBottom,
overflowTop = rect.top - menuHeight < 0,
overflowLeft = rect.left - menuWidth < 0,
overflowRight = rect.right + menuWidth > editorRight;
if (overflowTop && !overflowBottom) {
/// show at bottom
top = rect.bottom;
} else if (overflowBottom && !overflowTop) {
/// show at top
bottom = editorBottom - rect.top;
} else if (!overflowTop && !overflowBottom) {
/// show at bottom
top = rect.bottom;
} else {
top = 0;
}
if (overflowLeft && !overflowRight) {
/// show at right
left = rect.left;
} else if (overflowRight && !overflowLeft) {
/// show at left
right = editorRight - rect.right;
} else if (!overflowLeft && !overflowRight) {
/// show at right
left = rect.left;
} else {
left = 0;
}
if (left != null && top != null) {
alignment = LinkMenuAlignment.bottomRight;
} else if (left != null && bottom != null) {
alignment = LinkMenuAlignment.topRight;
} else if (right != null && top != null) {
alignment = LinkMenuAlignment.bottomLeft;
} else if (right != null && bottom != null) {
alignment = LinkMenuAlignment.topLeft;
}
return (left, top, right, bottom, alignment);
}
enum LinkMenuAlignment {
topLeft,
topRight,

View File

@ -1,5 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -43,5 +44,6 @@ ToolbarItem group1PaddingItem =
ToolbarItem group4PaddingItem = buildPaddingPlaceholderItem(
4,
isActive: onlyShowInSingleSelectionAndTextType,
isActive: (state) =>
!isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state),
);

View File

@ -1,8 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -11,7 +13,8 @@ import 'toolbar_id_enum.dart';
final ToolbarItem customTextAlignItem = ToolbarItem(
id: ToolbarId.textAlign.id,
group: 4,
isActive: onlyShowInSingleSelectionAndTextType,
isActive: (state) =>
!isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state),
builder: (
context,
editorState,
@ -33,18 +36,29 @@ class TextAlignActionList extends StatefulWidget {
required this.editorState,
required this.highlightColor,
this.tooltipBuilder,
this.child,
this.onSelect,
this.popoverController,
this.popoverDirection = PopoverDirection.bottomWithLeftAligned,
this.showOffset = const Offset(0, 2),
});
final EditorState editorState;
final ToolbarTooltipBuilder? tooltipBuilder;
final Color highlightColor;
final Widget? child;
final VoidCallback? onSelect;
final PopoverController? popoverController;
final PopoverDirection popoverDirection;
final Offset showOffset;
@override
State<TextAlignActionList> createState() => _TextAlignActionListState();
}
class _TextAlignActionListState extends State<TextAlignActionList> {
final popoverController = PopoverController();
late PopoverController popoverController =
widget.popoverController ?? PopoverController();
bool isSelected = false;
@ -62,8 +76,8 @@ class _TextAlignActionListState extends State<TextAlignActionList> {
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 2.0),
direction: widget.popoverDirection,
offset: widget.showOffset,
onOpen: () => keepEditorFocusNotifier.increase(),
onClose: () {
setState(() {
@ -72,7 +86,7 @@ class _TextAlignActionListState extends State<TextAlignActionList> {
keepEditorFocusNotifier.decrease();
},
popupBuilder: (context) => buildPopoverContent(),
child: buildChild(context),
child: widget.child ?? buildChild(context),
);
}
@ -82,7 +96,7 @@ class _TextAlignActionListState extends State<TextAlignActionList> {
}
Widget buildChild(BuildContext context) {
final iconColor = Theme.of(context).iconTheme.color;
final themeV2 = AFThemeExtensionV2.of(context);
final child = FlowyIconButton(
width: 48,
height: 32,
@ -94,13 +108,13 @@ class _TextAlignActionListState extends State<TextAlignActionList> {
FlowySvg(
FlowySvgs.toolbar_alignment_m,
size: Size.square(20),
color: iconColor,
color: themeV2.icon_primary,
),
HSpace(4),
FlowySvg(
FlowySvgs.toolbar_arrow_down_m,
size: Size(12, 20),
color: iconColor,
color: themeV2.icon_tertiary,
),
],
),
@ -149,6 +163,7 @@ class _TextAlignActionListState extends State<TextAlignActionList> {
isHighlight ? FlowySvg(FlowySvgs.toolbar_check_m) : null,
onTap: () {
command.onAlignChanged(editorState);
widget.onSelect?.call();
popoverController.close();
},
),

View File

@ -1,8 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
// ignore: implementation_imports
@ -10,6 +14,10 @@ import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_u
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'custom_text_align_toolbar_item.dart';
import 'text_suggestions_toolbar_item.dart';
const _kMoreOptionItemId = 'editor.more_option';
const kFontToolbarItemId = 'editor.font';
@ -54,7 +62,9 @@ class MoreOptionActionList extends StatefulWidget {
class _MoreOptionActionListState extends State<MoreOptionActionList> {
final popoverController = PopoverController();
final fontPopoverController = PopoverController();
PopoverController fontPopoverController = PopoverController();
PopoverController suggestionsPopoverController = PopoverController();
PopoverController textAlignPopoverController = PopoverController();
bool isSelected = false;
@ -62,11 +72,15 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
Color get highlightColor => widget.highlightColor;
MoreOptionCommand? tappedCommand;
@override
void dispose() {
super.dispose();
popoverController.close();
fontPopoverController.close();
suggestionsPopoverController.close();
textAlignPopoverController.close();
}
@override
@ -154,11 +168,17 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
Widget buildPopoverContent() {
final showFormula = onlyShowInSingleSelectionAndTextType(editorState);
const fontColor = Color(0xff99A1A8);
final isNarrow = isNarrowWindow(editorState);
return MouseRegion(
child: SeparatedColumn(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () => const VSpace(4.0),
children: [
if (isNarrow) ...[
buildTurnIntoSelector(),
buildCommandItem(MoreOptionCommand.link),
buildTextAlignSelector(),
],
buildFontSelector(),
buildCommandItem(
MoreOptionCommand.strikethrough,
@ -197,6 +217,7 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
Widget buildCommandItem(
MoreOptionCommand command, {
Widget? rightIcon,
VoidCallback? onTap,
}) {
final isFontCommand = command == MoreOptionCommand.font;
return SizedBox(
@ -212,12 +233,14 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
figmaLineHeight: 20,
fontWeight: FontWeight.w400,
),
onTap: () {
command.onExecute(editorState);
if (command != MoreOptionCommand.font) {
popoverController.close();
}
},
onTap: onTap ??
() {
command.onExecute(editorState, context);
hideOtherPopovers(command);
if (command != MoreOptionCommand.font) {
popoverController.close();
}
},
),
);
}
@ -255,9 +278,73 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
),
);
}
Widget buildTurnIntoSelector() {
final selectionRects = editorState.selectionRects();
double height = -6;
if (selectionRects.isNotEmpty) height = selectionRects.first.height;
return SuggestionsActionList(
editorState: editorState,
popoverController: suggestionsPopoverController,
popoverDirection: PopoverDirection.leftWithTopAligned,
showOffset: Offset(-8, height),
onSelect: () => context.read<ToolbarCubit?>()?.dismiss(),
child: buildCommandItem(
MoreOptionCommand.suggestions,
rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m),
onTap: () {
if (tappedCommand == MoreOptionCommand.suggestions) return;
hideOtherPopovers(MoreOptionCommand.suggestions);
keepEditorFocusNotifier.increase();
suggestionsPopoverController.show();
},
),
);
}
Widget buildTextAlignSelector() {
return TextAlignActionList(
editorState: editorState,
popoverController: textAlignPopoverController,
popoverDirection: PopoverDirection.leftWithTopAligned,
showOffset: Offset(-8, 0),
onSelect: () => context.read<ToolbarCubit?>()?.dismiss(),
highlightColor: highlightColor,
child: buildCommandItem(
MoreOptionCommand.textAlign,
rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m),
onTap: () {
if (tappedCommand == MoreOptionCommand.textAlign) return;
hideOtherPopovers(MoreOptionCommand.textAlign);
keepEditorFocusNotifier.increase();
textAlignPopoverController.show();
},
),
);
}
void hideOtherPopovers(MoreOptionCommand currentCommand) {
if (tappedCommand == currentCommand) return;
if (tappedCommand == MoreOptionCommand.font) {
fontPopoverController.close();
fontPopoverController = PopoverController();
} else if (tappedCommand == MoreOptionCommand.suggestions) {
suggestionsPopoverController.close();
suggestionsPopoverController = PopoverController();
} else if (tappedCommand == MoreOptionCommand.textAlign) {
textAlignPopoverController.close();
textAlignPopoverController = PopoverController();
}
tappedCommand = currentCommand;
}
}
enum MoreOptionCommand {
suggestions(FlowySvgs.turninto_s),
link(FlowySvgs.toolbar_link_m),
textAlign(
FlowySvgs.toolbar_alignment_m,
),
font(FlowySvgs.type_font_m),
strikethrough(FlowySvgs.type_strikethrough_m),
formula(FlowySvgs.type_formula_m);
@ -268,6 +355,12 @@ enum MoreOptionCommand {
String get title {
switch (this) {
case suggestions:
return LocaleKeys.document_toolbar_turnInto.tr();
case link:
return LocaleKeys.document_toolbar_link.tr();
case textAlign:
return LocaleKeys.button_align.tr();
case font:
return LocaleKeys.document_toolbar_font.tr();
case strikethrough:
@ -277,14 +370,26 @@ enum MoreOptionCommand {
}
}
Future<void> onExecute(EditorState editorState) async {
if (this == strikethrough) {
Future<void> onExecute(EditorState editorState, BuildContext context) async {
final selection = editorState.selection!;
if (this == link) {
final nodes = editorState.getNodesInSelection(selection);
final isHref = nodes.allSatisfyInSelection(selection, (delta) {
return delta.everyAttributes(
(attributes) => attributes[AppFlowyRichTextKeys.href] != null,
);
});
context.read<ToolbarCubit?>()?.dismiss();
if (isHref) {
getIt<LinkHoverTriggers>().call(
HoverTriggerKey(nodes.first.id, selection),
);
} else {
showLinkCreateMenu(context, editorState, selection);
}
} else if (this == strikethrough) {
await editorState.toggleAttribute(name);
} else if (this == formula) {
final selection = editorState.selection;
if (selection == null || selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
final delta = node?.delta;
if (node == null || delta == null) {

View File

@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -77,7 +78,7 @@ class _TextHeadingActionListState extends State<TextHeadingActionList> {
}
Widget buildChild(BuildContext context) {
final iconColor = Theme.of(context).iconTheme.color;
final themeV2 = AFThemeExtensionV2.of(context);
final child = FlowyIconButton(
width: 48,
height: 32,
@ -89,13 +90,13 @@ class _TextHeadingActionListState extends State<TextHeadingActionList> {
FlowySvg(
FlowySvgs.toolbar_text_format_m,
size: Size.square(20),
color: iconColor,
color: themeV2.icon_primary,
),
HSpace(4),
FlowySvg(
FlowySvgs.toolbar_arrow_down_m,
size: Size(12, 20),
color: iconColor,
color: themeV2.icon_tertiary,
),
],
),

View File

@ -1,14 +1,16 @@
import 'dart:collection';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
@ -45,17 +47,28 @@ class SuggestionsActionList extends StatefulWidget {
super.key,
required this.editorState,
this.tooltipBuilder,
this.child,
this.onSelect,
this.popoverController,
this.popoverDirection = PopoverDirection.bottomWithLeftAligned,
this.showOffset = const Offset(0, 2),
});
final EditorState editorState;
final ToolbarTooltipBuilder? tooltipBuilder;
final Widget? child;
final VoidCallback? onSelect;
final PopoverController? popoverController;
final PopoverDirection popoverDirection;
final Offset showOffset;
@override
State<SuggestionsActionList> createState() => _SuggestionsActionListState();
}
class _SuggestionsActionListState extends State<SuggestionsActionList> {
final popoverController = PopoverController();
late PopoverController popoverController =
widget.popoverController ?? PopoverController();
bool isSelected = false;
@ -83,8 +96,8 @@ class _SuggestionsActionListState extends State<SuggestionsActionList> {
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 2.0),
direction: widget.popoverDirection,
offset: widget.showOffset,
onOpen: () => keepEditorFocusNotifier.increase(),
onClose: () {
setState(() {
@ -94,7 +107,7 @@ class _SuggestionsActionListState extends State<SuggestionsActionList> {
},
constraints: const BoxConstraints(maxWidth: 240, maxHeight: 400),
popupBuilder: (context) => buildPopoverContent(context),
child: buildChild(context),
child: widget.child ?? buildChild(context),
);
}
@ -104,7 +117,8 @@ class _SuggestionsActionListState extends State<SuggestionsActionList> {
}
Widget buildChild(BuildContext context) {
final iconColor = Theme.of(context).iconTheme.color;
final themeV2 = AFThemeExtensionV2.of(context);
final child = FlowyHover(
isSelected: () => isSelected,
style: HoverStyle(
@ -147,7 +161,7 @@ class _SuggestionsActionListState extends State<SuggestionsActionList> {
FlowySvg(
FlowySvgs.toolbar_arrow_down_m,
size: Size(12, 20),
color: iconColor,
color: themeV2.icon_tertiary,
),
],
),
@ -206,7 +220,8 @@ class _SuggestionsActionListState extends State<SuggestionsActionList> {
),
rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null,
onTap: () {
item.onTap(widget.editorState);
item.onTap(widget.editorState, true);
widget.onSelect?.call();
popoverController.close();
},
),
@ -289,10 +304,10 @@ class SuggestionItem {
final SuggestionType type;
final String title;
final FlowySvgData svg;
final ValueChanged<EditorState> onTap;
final Function(EditorState state, bool keepSelection) onTap;
}
enum SuggestionGroup { textHeading, list, toggle, quote }
enum SuggestionGroup { textHeading, list, toggle, quote, page }
enum SuggestionType {
text(SuggestionGroup.textHeading),
@ -307,7 +322,8 @@ enum SuggestionType {
toggleH2(SuggestionGroup.toggle),
toggleH3(SuggestionGroup.toggle),
callOut(SuggestionGroup.quote),
quote(SuggestionGroup.quote);
quote(SuggestionGroup.quote),
page(SuggestionGroup.page);
const SuggestionType(this.group);
@ -318,94 +334,166 @@ final textSuggestionItem = SuggestionItem(
type: SuggestionType.text,
title: AppFlowyEditorL10n.current.text,
svg: FlowySvgs.type_text_m,
onTap: (state) => formatNodeToText(state),
onTap: (state, _) => formatNodeToText(state),
);
final h1SuggestionItem = SuggestionItem(
type: SuggestionType.h1,
title: LocaleKeys.document_toolbar_h1.tr(),
svg: FlowySvgs.type_h1_m,
onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 1),
onTap: (state, keepSelection) => _turnInto(
state,
HeadingBlockKeys.type,
level: 1,
keepSelection: keepSelection,
),
);
final h2SuggestionItem = SuggestionItem(
type: SuggestionType.h2,
title: LocaleKeys.document_toolbar_h2.tr(),
svg: FlowySvgs.type_h2_m,
onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 2),
onTap: (state, keepSelection) => _turnInto(
state,
HeadingBlockKeys.type,
level: 2,
keepSelection: keepSelection,
),
);
final h3SuggestionItem = SuggestionItem(
type: SuggestionType.h3,
title: LocaleKeys.document_toolbar_h3.tr(),
svg: FlowySvgs.type_h3_m,
onTap: (state) => _turnInto(state, HeadingBlockKeys.type, level: 3),
onTap: (state, keepSelection) => _turnInto(
state,
HeadingBlockKeys.type,
level: 3,
keepSelection: keepSelection,
),
);
final checkboxSuggestionItem = SuggestionItem(
type: SuggestionType.checkbox,
title: LocaleKeys.editor_checkbox.tr(),
svg: FlowySvgs.type_todo_m,
onTap: (state) => _turnInto(state, TodoListBlockKeys.type),
onTap: (state, keepSelection) => _turnInto(
state,
TodoListBlockKeys.type,
keepSelection: keepSelection,
),
);
final bulletedSuggestionItem = SuggestionItem(
type: SuggestionType.bulleted,
title: LocaleKeys.editor_bulletedListShortForm.tr(),
svg: FlowySvgs.type_bulleted_list_m,
onTap: (state) => _turnInto(state, BulletedListBlockKeys.type),
onTap: (state, keepSelection) => _turnInto(
state,
BulletedListBlockKeys.type,
keepSelection: keepSelection,
),
);
final numberedSuggestionItem = SuggestionItem(
type: SuggestionType.numbered,
title: LocaleKeys.editor_numberedListShortForm.tr(),
svg: FlowySvgs.type_numbered_list_m,
onTap: (state) => _turnInto(state, NumberedListBlockKeys.type),
onTap: (state, keepSelection) => _turnInto(
state,
NumberedListBlockKeys.type,
keepSelection: keepSelection,
),
);
final toggleSuggestionItem = SuggestionItem(
type: SuggestionType.toggle,
title: LocaleKeys.editor_toggleListShortForm.tr(),
svg: FlowySvgs.type_toggle_list_m,
onTap: (state) => _turnInto(state, ToggleListBlockKeys.type),
onTap: (state, keepSelection) => _turnInto(
state,
ToggleListBlockKeys.type,
keepSelection: keepSelection,
),
);
final toggleH1SuggestionItem = SuggestionItem(
type: SuggestionType.toggleH1,
title: LocaleKeys.editor_toggleHeading1ShortForm.tr(),
svg: FlowySvgs.type_toggle_h1_m,
onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 1),
onTap: (state, keepSelection) => _turnInto(
state,
ToggleListBlockKeys.type,
level: 1,
keepSelection: keepSelection,
),
);
final toggleH2SuggestionItem = SuggestionItem(
type: SuggestionType.toggleH2,
title: LocaleKeys.editor_toggleHeading2ShortForm.tr(),
svg: FlowySvgs.type_toggle_h2_m,
onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 2),
onTap: (state, keepSelection) => _turnInto(
state,
ToggleListBlockKeys.type,
level: 2,
keepSelection: keepSelection,
),
);
final toggleH3SuggestionItem = SuggestionItem(
type: SuggestionType.toggleH3,
title: LocaleKeys.editor_toggleHeading3ShortForm.tr(),
svg: FlowySvgs.type_toggle_h3_m,
onTap: (state) => _turnInto(state, ToggleListBlockKeys.type, level: 3),
onTap: (state, keepSelection) => _turnInto(
state,
ToggleListBlockKeys.type,
level: 3,
keepSelection: keepSelection,
),
);
final callOutSuggestionItem = SuggestionItem(
type: SuggestionType.callOut,
title: LocaleKeys.document_plugins_callout.tr(),
svg: FlowySvgs.type_callout_m,
onTap: (state) => _turnInto(state, CalloutBlockKeys.type),
onTap: (state, keepSelection) => _turnInto(
state,
CalloutBlockKeys.type,
keepSelection: keepSelection,
),
);
final quoteSuggestionItem = SuggestionItem(
type: SuggestionType.quote,
title: LocaleKeys.editor_quote.tr(),
svg: FlowySvgs.type_quote_m,
onTap: (state) => _turnInto(state, QuoteBlockKeys.type),
onTap: (state, keepSelection) => _turnInto(
state,
QuoteBlockKeys.type,
keepSelection: keepSelection,
),
);
Future<void> _turnInto(EditorState state, String type, {int? level}) async {
final pateItem = SuggestionItem(
type: SuggestionType.page,
title: LocaleKeys.editor_page.tr(),
svg: FlowySvgs.icon_document_s,
onTap: (state, keepSelection) => _turnInto(
state,
SubPageBlockKeys.type,
viewId: getIt<MenuSharedState>().latestOpenView?.id,
keepSelection: keepSelection,
),
);
Future<void> _turnInto(
EditorState state,
String type, {
int? level,
String? viewId,
bool keepSelection = true,
}) async {
final selection = state.selection!;
final node = state.getNodeAtPath(selection.start.path)!;
await BlockActionOptionCubit.turnIntoBlock(
@ -413,7 +501,8 @@ Future<void> _turnInto(EditorState state, String type, {int? level}) async {
node,
state,
level: level,
keepSelection: true,
currentViewId: viewId,
keepSelection: keepSelection,
);
}
@ -431,6 +520,7 @@ final suggestions = UnmodifiableListView([
toggleH3SuggestionItem,
callOutSuggestionItem,
quoteSuggestionItem,
pateItem,
]);
final nodeType2SuggestionType = UnmodifiableMapView({

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'package:appflowy/util/expand_views.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
@ -185,6 +186,7 @@ Future<void> initGetIt(
);
getIt.registerSingleton<PluginSandbox>(PluginSandbox());
getIt.registerSingleton<ViewExpanderRegistry>(ViewExpanderRegistry());
getIt.registerSingleton<LinkHoverTriggers>(LinkHoverTriggers());
await DependencyResolver.resolve(getIt, mode);
}

View File

@ -2,6 +2,7 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/theme_extension_v2.dart';
import 'package:flutter/material.dart';
class DesktopAppearance extends BaseAppearance {
@ -14,9 +15,8 @@ class DesktopAppearance extends BaseAppearance {
) {
assert(codeFontFamily.isNotEmpty);
final theme = brightness == Brightness.light
? appTheme.lightTheme
: appTheme.darkTheme;
final isLight = brightness == Brightness.light;
final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme;
final colorScheme = ColorScheme(
brightness: brightness,
@ -152,6 +152,11 @@ class DesktopAppearance extends BaseAppearance {
lightIconColor: theme.lightIconColor,
toolbarHoverColor: theme.toolbarHoverColor,
),
isLight
? lightAFThemeV2
: darkAFThemeV2.copyWith(
icon_primary: theme.icon,
),
],
);
}

View File

@ -0,0 +1,91 @@
// ignore_for_file: non_constant_identifier_names
import 'package:flutter/material.dart';
@immutable
class AFThemeExtensionV2 extends ThemeExtension<AFThemeExtensionV2> {
static AFThemeExtensionV2 of(BuildContext context) =>
Theme.of(context).extension<AFThemeExtensionV2>()!;
static AFThemeExtensionV2? maybeOf(BuildContext context) =>
Theme.of(context).extension<AFThemeExtensionV2>();
const AFThemeExtensionV2({
required this.icon_primary,
required this.icon_tertiary,
required this.border_grey_quaternary,
required this.fill_theme_select,
required this.fill_grey_thick_alpha_1,
required this.shadow_medium,
});
final Color icon_primary;
final Color icon_tertiary;
final Color border_grey_quaternary;
final Color fill_theme_select;
final Color fill_grey_thick_alpha_1;
final Color shadow_medium;
@override
AFThemeExtensionV2 copyWith({
Color? icon_primary,
Color? icon_tertiary,
Color? border_grey_quaternary,
Color? fill_theme_select,
Color? fill_grey_thick_alpha_1,
Color? shadow_medium,
}) =>
AFThemeExtensionV2(
icon_primary: icon_primary ?? this.icon_primary,
icon_tertiary: icon_tertiary ?? this.icon_tertiary,
border_grey_quaternary:
border_grey_quaternary ?? this.border_grey_quaternary,
fill_theme_select: fill_theme_select ?? this.fill_theme_select,
fill_grey_thick_alpha_1:
fill_grey_thick_alpha_1 ?? this.fill_grey_thick_alpha_1,
shadow_medium: shadow_medium ?? this.shadow_medium,
);
@override
ThemeExtension<AFThemeExtensionV2> lerp(
ThemeExtension<AFThemeExtensionV2>? other, double t) {
if (other is! AFThemeExtensionV2) {
return this;
}
return AFThemeExtensionV2(
icon_primary:
Color.lerp(icon_primary, other.icon_primary, t) ?? icon_primary,
icon_tertiary:
Color.lerp(icon_tertiary, other.icon_tertiary, t) ?? icon_tertiary,
border_grey_quaternary:
Color.lerp(border_grey_quaternary, other.border_grey_quaternary, t) ??
border_grey_quaternary,
fill_theme_select:
Color.lerp(fill_theme_select, other.fill_theme_select, t) ??
fill_theme_select,
fill_grey_thick_alpha_1: Color.lerp(
fill_grey_thick_alpha_1, other.fill_grey_thick_alpha_1, t) ??
fill_grey_thick_alpha_1,
shadow_medium:
Color.lerp(shadow_medium, other.shadow_medium, t) ?? shadow_medium,
);
}
}
const AFThemeExtensionV2 darkAFThemeV2 = AFThemeExtensionV2(
icon_primary: Color(0xFF1F2329),
icon_tertiary: Color(0xFF99A1A8),
border_grey_quaternary: Color(0xFFE8ECF3),
fill_theme_select: Color(0x00BCF01F),
fill_grey_thick_alpha_1: Color(0x1F23290F),
shadow_medium: Color(0x1F22251F),
);
const AFThemeExtensionV2 lightAFThemeV2 = AFThemeExtensionV2(
icon_primary: Color(0xFF1F2329),
icon_tertiary: Color(0xFF99A1A8),
border_grey_quaternary: Color(0xFFE8ECF3),
fill_theme_select: Color(0x00BCF01F),
fill_grey_thick_alpha_1: Color(0x1F23290F),
shadow_medium: Color(0x1F22251F),
);

View File

@ -98,8 +98,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "8f314fd"
resolved-ref: "8f314fda5981e650a52ba522ba7915e13940d837"
ref: "5ad9d77"
resolved-ref: "5ad9d771a8496dea95a9c5b1ec77f76df3983037"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "5.1.0"

View File

@ -184,7 +184,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "8f314fd"
ref: "5ad9d77"
appflowy_editor_plugins:
git: