mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-09 22:30:35 +00:00
fix: toolbar link launch review issues (#7639)
* fix: some toolbar link launch review issues * fix: support check link format for link menus * fix: toolbar and link hover menu will not display together * fix: filter link search result with current document id * fix: remove error text while link menu is not focus * fix: some issues * fix: test errors * fix: add tooltip for link menu
This commit is contained in:
parent
3c99105b23
commit
5e1a8b1ec7
@ -432,44 +432,50 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: FloatingToolbar(
|
||||
floatingToolbarHeight: 40,
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
style: FloatingToolbarStyle(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
toolbarActiveColor: Color(0xffe0f8fd),
|
||||
),
|
||||
items: toolbarItems,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 24,
|
||||
color: themeV2.shadow_medium,
|
||||
child: BlocProvider.value(
|
||||
value: context.read<DocumentBloc>(),
|
||||
child: FloatingToolbar(
|
||||
floatingToolbarHeight: 40,
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
style: FloatingToolbarStyle(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
toolbarActiveColor: Color(0xffe0f8fd),
|
||||
),
|
||||
items: toolbarItems,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 24,
|
||||
color: themeV2.shadow_medium,
|
||||
),
|
||||
],
|
||||
),
|
||||
toolbarBuilder: (_, child, onDismiss, isMetricsChanged) =>
|
||||
BlocProvider.value(
|
||||
value: context.read<DocumentBloc>(),
|
||||
child: DesktopFloatingToolbar(
|
||||
editorState: editorState,
|
||||
onDismiss: onDismiss,
|
||||
enableAnimation: false,
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
toolbarBuilder: (context, child, onDismiss, isMetricsChanged) =>
|
||||
DesktopFloatingToolbar(
|
||||
),
|
||||
placeHolderBuilder: (_) => customPlaceholderItem,
|
||||
editorState: editorState,
|
||||
onDismiss: onDismiss,
|
||||
enableAnimation: !isMetricsChanged,
|
||||
child: child,
|
||||
editorScrollController: editorScrollController,
|
||||
textDirection: textDirection,
|
||||
tooltipBuilder: (context, id, message, child) =>
|
||||
widget.styleCustomizer.buildToolbarItemTooltip(
|
||||
context,
|
||||
id,
|
||||
message,
|
||||
child,
|
||||
),
|
||||
child: editor,
|
||||
),
|
||||
placeHolderBuilder: (_) => customPlaceholderItem,
|
||||
editorState: editorState,
|
||||
editorScrollController: editorScrollController,
|
||||
textDirection: textDirection,
|
||||
tooltipBuilder: (context, id, message, child) =>
|
||||
widget.styleCustomizer.buildToolbarItemTooltip(
|
||||
context,
|
||||
id,
|
||||
message,
|
||||
child,
|
||||
),
|
||||
child: editor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'toolbar_animation.dart';
|
||||
import 'toolbar_cubit.dart';
|
||||
|
||||
class DesktopFloatingToolbar extends StatefulWidget {
|
||||
const DesktopFloatingToolbar({
|
||||
@ -28,6 +27,7 @@ class _DesktopFloatingToolbarState extends State<DesktopFloatingToolbar> {
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
_Position? position;
|
||||
final toolbarController = getIt<FloatingToolbarController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -39,24 +39,32 @@ class _DesktopFloatingToolbarState extends State<DesktopFloatingToolbar> {
|
||||
final selectionRect = editorState.selectionRects();
|
||||
if (selectionRect.isEmpty) return;
|
||||
position = calculateSelectionMenuOffset(selectionRect.first);
|
||||
toolbarController._addCallback(dismiss);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
toolbarController._removeCallback(dismiss);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (position == null) return Container();
|
||||
return BlocProvider<ToolbarCubit>(
|
||||
create: (_) => ToolbarCubit(widget.onDismiss),
|
||||
child: Positioned(
|
||||
left: position!.left,
|
||||
top: position!.top,
|
||||
right: position!.right,
|
||||
child: widget.enableAnimation
|
||||
? ToolbarAnimationWidget(child: widget.child)
|
||||
: widget.child,
|
||||
),
|
||||
return Positioned(
|
||||
left: position!.left,
|
||||
top: position!.top,
|
||||
right: position!.right,
|
||||
child: widget.enableAnimation
|
||||
? ToolbarAnimationWidget(child: widget.child)
|
||||
: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
widget.onDismiss.call();
|
||||
}
|
||||
|
||||
_Position calculateSelectionMenuOffset(
|
||||
Rect rect,
|
||||
) {
|
||||
@ -92,3 +100,33 @@ class _Position {
|
||||
final double? top;
|
||||
final double? right;
|
||||
}
|
||||
|
||||
class FloatingToolbarController {
|
||||
final Set<VoidCallback> _dismissCallbacks = {};
|
||||
final Set<VoidCallback> _displayListeners = {};
|
||||
|
||||
void _addCallback(VoidCallback callback) {
|
||||
_dismissCallbacks.add(callback);
|
||||
for (final listener in Set.of(_displayListeners)) {
|
||||
listener.call();
|
||||
}
|
||||
}
|
||||
|
||||
void _removeCallback(VoidCallback callback) =>
|
||||
_dismissCallbacks.remove(callback);
|
||||
|
||||
bool get isToolbarShowing => _dismissCallbacks.isNotEmpty;
|
||||
|
||||
void addDisplayListener(VoidCallback listener) =>
|
||||
_displayListeners.add(listener);
|
||||
|
||||
void removeDisplayListener(VoidCallback listener) =>
|
||||
_displayListeners.remove(listener);
|
||||
|
||||
void hideToolbar() {
|
||||
if (_dismissCallbacks.isEmpty) return;
|
||||
for (final callback in _dismissCallbacks) {
|
||||
callback.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,11 +19,15 @@ class LinkCreateMenu extends StatefulWidget {
|
||||
required this.onSubmitted,
|
||||
required this.onDismiss,
|
||||
required this.alignment,
|
||||
required this.currentViewId,
|
||||
required this.initialText,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final void Function(String link, bool isPage) onSubmitted;
|
||||
final VoidCallback onDismiss;
|
||||
final String currentViewId;
|
||||
final String initialText;
|
||||
final LinkMenuAlignment alignment;
|
||||
|
||||
@override
|
||||
@ -32,6 +36,8 @@ class LinkCreateMenu extends StatefulWidget {
|
||||
|
||||
class _LinkCreateMenuState extends State<LinkCreateMenu> {
|
||||
late LinkSearchTextField searchTextField = LinkSearchTextField(
|
||||
currentViewId: widget.currentViewId,
|
||||
initialSearchText: widget.initialText,
|
||||
onEnter: () {
|
||||
searchTextField.onSearchResult(
|
||||
onLink: () => onSubmittedLink(),
|
||||
@ -48,17 +54,28 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
|
||||
},
|
||||
);
|
||||
|
||||
bool get isButtonEnable => searchText.isNotEmpty;
|
||||
bool get isTextfieldEnable => searchTextField.isTextfieldEnable;
|
||||
|
||||
String get searchText => searchTextField.searchText;
|
||||
|
||||
bool get showAtTop => widget.alignment.isTop;
|
||||
|
||||
bool showErrorText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
searchTextField.requestFocus();
|
||||
searchTextField.searchRecentViews();
|
||||
final focusNode = searchTextField.focusNode;
|
||||
bool hasFocus = focusNode.hasFocus;
|
||||
focusNode.addListener(() {
|
||||
if (hasFocus != focusNode.hasFocus && mounted) {
|
||||
setState(() {
|
||||
hasFocus = focusNode.hasFocus;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -98,32 +115,45 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
|
||||
Widget buildSearchContainer() {
|
||||
return Container(
|
||||
width: 320,
|
||||
height: 48,
|
||||
decoration: buildToolbarLinkDecoration(context),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: searchTextField.textEditingController,
|
||||
builder: (context, _, __) {
|
||||
return Row(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: searchTextField.buildTextField()),
|
||||
HSpace(8),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.document_toolbar_insert.tr(),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(maxWidth: 72, minHeight: 32),
|
||||
fontSize: 14,
|
||||
fontColor:
|
||||
isButtonEnable ? Colors.white : LinkStyle.textTertiary,
|
||||
fillColor: isButtonEnable
|
||||
? LinkStyle.fillThemeThick
|
||||
: LinkStyle.borderColor,
|
||||
hoverColor: LinkStyle.fillThemeThick,
|
||||
lineHeight: 20 / 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
onPressed: isButtonEnable ? () => onSubmittedLink() : null,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: searchTextField.buildTextField(context: context),
|
||||
),
|
||||
HSpace(8),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.document_toolbar_insert.tr(),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(maxWidth: 72, minHeight: 32),
|
||||
fontSize: 14,
|
||||
fontColor: Colors.white,
|
||||
fillColor: LinkStyle.fillThemeThick,
|
||||
hoverColor: LinkStyle.fillThemeThick.withAlpha(200),
|
||||
lineHeight: 20 / 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
onPressed: onSubmittedLink,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showErrorText)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),
|
||||
color: LinkStyle.textStatusError,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -131,7 +161,15 @@ class _LinkCreateMenuState extends State<LinkCreateMenu> {
|
||||
);
|
||||
}
|
||||
|
||||
void onSubmittedLink() => widget.onSubmitted(searchText, false);
|
||||
void onSubmittedLink() {
|
||||
if (!isTextfieldEnable) {
|
||||
setState(() {
|
||||
showErrorText = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
widget.onSubmitted(searchText, false);
|
||||
}
|
||||
|
||||
void onSubmittedPageLink(ViewPB view) async {
|
||||
final workspaceId = context
|
||||
@ -152,13 +190,16 @@ void showLinkCreateMenu(
|
||||
BuildContext context,
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
String currentViewId,
|
||||
) {
|
||||
if (!context.mounted) return;
|
||||
final (left, top, right, bottom, alignment) = _getPosition(editorState);
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final selectedText = editorState.getTextInSelection(selection).join();
|
||||
|
||||
OverlayEntry? overlay;
|
||||
|
||||
@ -178,12 +219,18 @@ void showLinkCreateMenu(
|
||||
builder: (context) {
|
||||
return LinkCreateMenu(
|
||||
alignment: alignment,
|
||||
initialText: selectedText,
|
||||
currentViewId: currentViewId,
|
||||
editorState: editorState,
|
||||
onSubmitted: (link, isPage) async {
|
||||
await editorState.formatDelta(selection, {
|
||||
BuiltInAttributeKey.href: link,
|
||||
kIsPageLink: isPage,
|
||||
});
|
||||
await editorState.updateSelectionWithReason(
|
||||
null,
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
);
|
||||
dismissOverlay();
|
||||
},
|
||||
onDismiss: dismissOverlay,
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -10,7 +11,8 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// ignore: implementation_imports
|
||||
import 'package:appflowy_editor/src/editor/util/link_util.dart';
|
||||
import 'link_create_menu.dart';
|
||||
import 'link_search_text_field.dart';
|
||||
import 'link_styles.dart';
|
||||
@ -22,12 +24,14 @@ class LinkEditMenu extends StatefulWidget {
|
||||
required this.onDismiss,
|
||||
required this.onApply,
|
||||
required this.onRemoveLink,
|
||||
required this.currentViewId,
|
||||
});
|
||||
|
||||
final LinkInfo linkInfo;
|
||||
final ValueChanged<LinkInfo> onApply;
|
||||
final VoidCallback onRemoveLink;
|
||||
final ValueChanged<LinkInfo> onRemoveLink;
|
||||
final VoidCallback onDismiss;
|
||||
final String currentViewId;
|
||||
|
||||
@override
|
||||
State<LinkEditMenu> createState() => _LinkEditMenuState();
|
||||
@ -36,7 +40,7 @@ class LinkEditMenu extends StatefulWidget {
|
||||
class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
ValueChanged<LinkInfo> get onApply => widget.onApply;
|
||||
|
||||
VoidCallback get onRemoveLink => widget.onRemoveLink;
|
||||
ValueChanged<LinkInfo> get onRemoveLink => widget.onRemoveLink;
|
||||
|
||||
VoidCallback get onDismiss => widget.onDismiss;
|
||||
|
||||
@ -47,9 +51,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
late LinkSearchTextField searchTextField;
|
||||
bool isShowingSearchResult = false;
|
||||
ViewPB? currentView;
|
||||
|
||||
bool get enableApply =>
|
||||
linkInfo.link.isNotEmpty && linkNameController.text.isNotEmpty;
|
||||
bool showErrorText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -58,18 +60,9 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
if (isPageLink) getPageView();
|
||||
searchTextField = LinkSearchTextField(
|
||||
initialSearchText: isPageLink ? '' : linkInfo.link,
|
||||
onEnter: () {
|
||||
searchTextField.onSearchResult(
|
||||
onLink: onLinkSelected,
|
||||
onRecentViews: () =>
|
||||
onPageSelected(searchTextField.currentRecentView),
|
||||
onSearchViews: () =>
|
||||
onPageSelected(searchTextField.currentSearchedView),
|
||||
onEmpty: () {
|
||||
searchTextField.unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
initialViewId: linkInfo.viewId,
|
||||
currentViewId: widget.currentViewId,
|
||||
onEnter: onConfirm,
|
||||
onEscape: () {
|
||||
if (isShowingSearchResult) {
|
||||
hideSearchResult();
|
||||
@ -95,6 +88,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
Widget build(BuildContext context) {
|
||||
final showingRecent =
|
||||
searchTextField.showingRecent && isShowingSearchResult;
|
||||
final errorHeight = showErrorText ? 20.0 : 0.0;
|
||||
return GestureDetector(
|
||||
onTap: onDismiss,
|
||||
child: Container(
|
||||
@ -107,9 +101,8 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
onTap: hideSearchResult,
|
||||
child: Container(
|
||||
width: 400,
|
||||
height: 192,
|
||||
height: 192 + errorHeight,
|
||||
decoration: buildToolbarLinkDecoration(context),
|
||||
padding: EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@ -123,7 +116,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 80,
|
||||
top: 80 + errorHeight,
|
||||
left: 20,
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.document_toolbar_linkName.tr(),
|
||||
@ -133,12 +126,12 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 152,
|
||||
top: 144 + errorHeight,
|
||||
left: 20,
|
||||
child: buildButtons(),
|
||||
),
|
||||
Positioned(
|
||||
top: 108,
|
||||
top: 100 + errorHeight,
|
||||
left: 20,
|
||||
child: buildNameTextField(),
|
||||
),
|
||||
@ -155,29 +148,53 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
|
||||
Widget buildLinkField() {
|
||||
final showPageView = linkInfo.isPage && !isShowingSearchResult;
|
||||
if (showPageView) return buildPageView();
|
||||
if (!isShowingSearchResult) return buildLinkView();
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 360,
|
||||
height: 32,
|
||||
child: searchTextField.buildTextField(
|
||||
autofocus: true,
|
||||
Widget child;
|
||||
if (showPageView) {
|
||||
child = buildPageView();
|
||||
} else if (!isShowingSearchResult) {
|
||||
child = buildLinkView();
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 360,
|
||||
height: 32,
|
||||
child: searchTextField.buildTextField(
|
||||
autofocus: true,
|
||||
context: context,
|
||||
),
|
||||
),
|
||||
VSpace(6),
|
||||
searchTextField.buildResultContainer(
|
||||
context: context,
|
||||
width: 360,
|
||||
onPageLinkSelected: onPageSelected,
|
||||
onLinkSelected: onLinkSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
child,
|
||||
if (showErrorText)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),
|
||||
color: LinkStyle.textStatusError,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
),
|
||||
),
|
||||
VSpace(6),
|
||||
searchTextField.buildResultContainer(
|
||||
context: context,
|
||||
width: 360,
|
||||
onPageLinkSelected: onPageSelected,
|
||||
onLinkSelected: onLinkSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -197,15 +214,15 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
preferBelow: false,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: LinkStyle.borderColor),
|
||||
border: Border.all(color: LinkStyle.borderColor(context)),
|
||||
),
|
||||
onPressed: onRemoveLink,
|
||||
onPressed: () => onRemoveLink.call(linkInfo),
|
||||
),
|
||||
Spacer(),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: LinkStyle.borderColor),
|
||||
border: Border.all(color: LinkStyle.borderColor(context)),
|
||||
),
|
||||
child: FlowyTextButton(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
@ -214,7 +231,9 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
constraints: BoxConstraints(maxWidth: 78, minHeight: 32),
|
||||
fontSize: 14,
|
||||
lineHeight: 20 / 14,
|
||||
fontColor: LinkStyle.textPrimary,
|
||||
fontColor: Theme.of(context).isLightMode
|
||||
? LinkStyle.textPrimary
|
||||
: Theme.of(context).iconTheme.color,
|
||||
fillColor: Colors.transparent,
|
||||
fontWeight: FontWeight.w400,
|
||||
onPressed: onDismiss,
|
||||
@ -232,14 +251,26 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
fontSize: 14,
|
||||
lineHeight: 20 / 14,
|
||||
hoverColor: LinkStyle.fillThemeThick.withAlpha(200),
|
||||
fontColor:
|
||||
enableApply ? Colors.white : LinkStyle.textTertiary,
|
||||
fillColor: enableApply
|
||||
? LinkStyle.fillThemeThick
|
||||
: LinkStyle.borderColor,
|
||||
fontColor: Colors.white,
|
||||
fillColor: LinkStyle.fillThemeThick,
|
||||
fontWeight: FontWeight.w400,
|
||||
onPressed:
|
||||
enableApply ? () => widget.onApply.call(linkInfo) : null,
|
||||
onPressed: () {
|
||||
if (isShowingSearchResult) {
|
||||
onConfirm();
|
||||
return;
|
||||
}
|
||||
if (linkInfo.link.isEmpty) {
|
||||
widget.onRemoveLink(linkInfo);
|
||||
return;
|
||||
}
|
||||
if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) {
|
||||
setState(() {
|
||||
showErrorText = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
widget.onApply.call(linkInfo);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -272,6 +303,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
},
|
||||
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
|
||||
LocaleKeys.document_toolbar_linkNameHint.tr(),
|
||||
context,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -288,24 +320,34 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final viewName = view.name;
|
||||
final displayName = viewName.isEmpty
|
||||
? LocaleKeys.document_title_placeholder.tr()
|
||||
: viewName;
|
||||
child = GestureDetector(
|
||||
onTap: showSearchResult,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Container(
|
||||
height: 32,
|
||||
padding: EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
searchTextField.buildIcon(view),
|
||||
HSpace(8),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
view.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: FlowyTooltip(
|
||||
preferBelow: false,
|
||||
message: displayName,
|
||||
child: Container(
|
||||
height: 32,
|
||||
padding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
searchTextField.buildIcon(view),
|
||||
HSpace(4),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -324,24 +366,28 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
width: 360,
|
||||
height: 32,
|
||||
decoration: buildDecoration(),
|
||||
child: GestureDetector(
|
||||
onTap: showSearchResult,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(FlowySvgs.toolbar_link_earth_m),
|
||||
HSpace(8),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
linkInfo.link,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
child: FlowyTooltip(
|
||||
preferBelow: false,
|
||||
message: linkInfo.link,
|
||||
child: GestureDetector(
|
||||
onTap: showSearchResult,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(FlowySvgs.toolbar_link_earth_m),
|
||||
HSpace(8),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
linkInfo.link,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -349,12 +395,21 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
);
|
||||
}
|
||||
|
||||
void onConfirm() {
|
||||
searchTextField.onSearchResult(
|
||||
onLink: onLinkSelected,
|
||||
onRecentViews: () => onPageSelected(searchTextField.currentRecentView),
|
||||
onSearchViews: () => onPageSelected(searchTextField.currentSearchedView),
|
||||
onEmpty: () {
|
||||
searchTextField.unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getPageView() async {
|
||||
if (!linkInfo.isPage) return;
|
||||
final link = linkInfo.link;
|
||||
final viewId = link.split('/').lastOrNull ?? '';
|
||||
final (view, isInTrash, isDeleted) =
|
||||
await ViewBackendService.getMentionPageStatus(viewId);
|
||||
await ViewBackendService.getMentionPageStatus(linkInfo.viewId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
currentView = view;
|
||||
@ -413,7 +468,7 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
|
||||
BoxDecoration buildDecoration() => BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: LinkStyle.borderColor),
|
||||
border: Border.all(color: LinkStyle.borderColor(context)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -426,4 +481,6 @@ class LinkInfo {
|
||||
|
||||
Attributes toAttribute() =>
|
||||
{AppFlowyRichTextKeys.href: link, kIsPageLink: isPage};
|
||||
|
||||
String get viewId => isPage ? link.split('/').lastOrNull ?? '' : '';
|
||||
}
|
||||
|
||||
@ -3,20 +3,25 @@ import 'dart:math';
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'link_create_menu.dart';
|
||||
import 'link_edit_menu.dart';
|
||||
import 'link_styles.dart';
|
||||
|
||||
class LinkHoverTrigger extends StatefulWidget {
|
||||
const LinkHoverTrigger({
|
||||
@ -45,6 +50,7 @@ class LinkHoverTrigger extends StatefulWidget {
|
||||
class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
final hoverMenuController = PopoverController();
|
||||
final editMenuController = PopoverController();
|
||||
final toolbarController = getIt<FloatingToolbarController>();
|
||||
bool isHoverMenuShowing = false;
|
||||
bool isHoverMenuHovering = false;
|
||||
bool isHoverTriggerHovering = false;
|
||||
@ -57,12 +63,13 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
|
||||
Attributes get attribute => widget.attribute;
|
||||
|
||||
HoverTriggerKey get triggerKey => HoverTriggerKey(widget.node.id, selection);
|
||||
late HoverTriggerKey triggerKey = HoverTriggerKey(widget.node.id, selection);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getIt<LinkHoverTriggers>()._add(triggerKey, showLinkHoverMenu);
|
||||
toolbarController.addDisplayListener(onToolbarShow);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -70,6 +77,7 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
hoverMenuController.close();
|
||||
editMenuController.close();
|
||||
getIt<LinkHoverTriggers>()._remove(triggerKey, showLinkHoverMenu);
|
||||
toolbarController.removeDisplayListener(onToolbarShow);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -132,9 +140,9 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
tryToDismissLinkHoverMenu();
|
||||
},
|
||||
onOpenLink: openLink,
|
||||
onCopyLink: copyLink,
|
||||
onCopyLink: () => copyLink(context),
|
||||
onEditLink: showLinkEditMenu,
|
||||
onRemoveLink: () => removeLink(editorState, selection, true),
|
||||
onRemoveLink: () => removeLink(editorState, selection),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
@ -144,6 +152,7 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
final href = attribute.href ?? '',
|
||||
isPage = attribute.isPage,
|
||||
title = editorState.getTextInSelection(selection).join();
|
||||
final currentViewId = context.read<DocumentBloc?>()?.documentId ?? '';
|
||||
return AppFlowyPopover(
|
||||
controller: editMenuController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
@ -159,6 +168,7 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
minHeight: 282,
|
||||
),
|
||||
popupBuilder: (context) => LinkEditMenu(
|
||||
currentViewId: currentViewId,
|
||||
linkInfo: LinkInfo(name: title, link: href, isPage: isPage),
|
||||
onDismiss: () => editMenuController.close(),
|
||||
onApply: (info) async {
|
||||
@ -173,14 +183,19 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
editMenuController.close();
|
||||
await editorState.apply(transaction);
|
||||
},
|
||||
onRemoveLink: () => removeLink(editorState, selection, true),
|
||||
onRemoveLink: (linkinfo) =>
|
||||
onRemoveAndReplaceLink(editorState, selection, linkinfo.name),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void onToolbarShow() => hoverMenuController.close();
|
||||
|
||||
void showLinkHoverMenu() {
|
||||
if (isHoverMenuShowing) return;
|
||||
if (isHoverMenuShowing || toolbarController.isToolbarShowing || !mounted) {
|
||||
return;
|
||||
}
|
||||
keepEditorFocusNotifier.increase();
|
||||
hoverMenuController.show();
|
||||
}
|
||||
@ -219,20 +234,24 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> copyLink() async {
|
||||
Future<void> copyLink(BuildContext context) async {
|
||||
final href = widget.attribute.href ?? '';
|
||||
if (href.isEmpty) return;
|
||||
await getIt<ClipboardService>()
|
||||
.setData(ClipboardServiceData(plainText: href));
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
hoverMenuController.close();
|
||||
}
|
||||
|
||||
void removeLink(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
bool isHref,
|
||||
) {
|
||||
if (!isHref) return;
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
@ -251,9 +270,34 @@ class _LinkHoverTriggerState extends State<LinkHoverTrigger> {
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
|
||||
void onRemoveAndReplaceLink(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
String text,
|
||||
) {
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final index = selection.normalized.startIndex;
|
||||
final length = selection.length;
|
||||
final transaction = editorState.transaction
|
||||
..replaceText(
|
||||
node,
|
||||
index,
|
||||
length,
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.href: null,
|
||||
kIsPageLink: null,
|
||||
},
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
class LinkHoverMenu extends StatelessWidget {
|
||||
class LinkHoverMenu extends StatefulWidget {
|
||||
const LinkHoverMenu({
|
||||
super.key,
|
||||
required this.attribute,
|
||||
@ -275,17 +319,31 @@ class LinkHoverMenu extends StatelessWidget {
|
||||
final VoidCallback onEditLink;
|
||||
final VoidCallback onRemoveLink;
|
||||
|
||||
@override
|
||||
State<LinkHoverMenu> createState() => _LinkHoverMenuState();
|
||||
}
|
||||
|
||||
class _LinkHoverMenuState extends State<LinkHoverMenu> {
|
||||
ViewPB? currentView;
|
||||
late bool isPage = widget.attribute.isPage;
|
||||
late String href = widget.attribute.href ?? '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (isPage) getPageView();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final href = attribute.href ?? '';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MouseRegion(
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
onEnter: widget.onEnter,
|
||||
onExit: widget.onExit,
|
||||
child: SizedBox(
|
||||
width: max(320, triggerSize.width),
|
||||
width: max(320, widget.triggerSize.width),
|
||||
height: 48,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
@ -293,21 +351,15 @@ class LinkHoverMenu extends StatelessWidget {
|
||||
width: 320,
|
||||
height: 48,
|
||||
decoration: buildToolbarLinkDecoration(context),
|
||||
padding: EdgeInsets.all(8),
|
||||
padding: EdgeInsets.fromLTRB(12, 8, 8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
href,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Expanded(child: buildLinkWidget()),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: LinkStyle.borderColor,
|
||||
color: Color(0xffE8ECF3)
|
||||
.withAlpha(Theme.of(context).isLightMode ? 255 : 40),
|
||||
margin: EdgeInsets.symmetric(horizontal: 6),
|
||||
),
|
||||
FlowyIconButton(
|
||||
@ -315,21 +367,21 @@ class LinkHoverMenu extends StatelessWidget {
|
||||
tooltipText: LocaleKeys.editor_copyLink.tr(),
|
||||
width: 36,
|
||||
height: 32,
|
||||
onPressed: onCopyLink,
|
||||
onPressed: widget.onCopyLink,
|
||||
),
|
||||
FlowyIconButton(
|
||||
icon: FlowySvg(FlowySvgs.toolbar_link_edit_m),
|
||||
tooltipText: LocaleKeys.editor_editLink.tr(),
|
||||
width: 36,
|
||||
height: 32,
|
||||
onPressed: onEditLink,
|
||||
onPressed: widget.onEditLink,
|
||||
),
|
||||
FlowyIconButton(
|
||||
icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m),
|
||||
tooltipText: LocaleKeys.editor_removeLink.tr(),
|
||||
width: 36,
|
||||
height: 32,
|
||||
onPressed: onRemoveLink,
|
||||
onPressed: widget.onRemoveLink,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -339,13 +391,13 @@ class LinkHoverMenu extends StatelessWidget {
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
onEnter: widget.onEnter,
|
||||
onExit: widget.onExit,
|
||||
child: GestureDetector(
|
||||
onTap: onOpenLink,
|
||||
onTap: widget.onOpenLink,
|
||||
child: Container(
|
||||
width: triggerSize.width,
|
||||
height: triggerSize.height,
|
||||
width: widget.triggerSize.width,
|
||||
height: widget.triggerSize.height,
|
||||
color: Colors.black.withAlpha(1),
|
||||
),
|
||||
),
|
||||
@ -353,6 +405,46 @@ class LinkHoverMenu extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getPageView() async {
|
||||
final viewId = href.split('/').lastOrNull ?? '';
|
||||
final (view, isInTrash, isDeleted) =
|
||||
await ViewBackendService.getMentionPageStatus(viewId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
currentView = view;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildLinkWidget() {
|
||||
final view = currentView;
|
||||
if (isPage && view == null) {
|
||||
return SizedBox.square(
|
||||
dimension: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
String text = '';
|
||||
if (isPage && view != null) {
|
||||
text = view.name;
|
||||
if (text.isEmpty) {
|
||||
text = LocaleKeys.document_title_placeholder.tr();
|
||||
}
|
||||
} else {
|
||||
text = href;
|
||||
}
|
||||
return FlowyTooltip(
|
||||
message: text,
|
||||
preferBelow: false,
|
||||
child: FlowyText.regular(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
fontSize: 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HoverTriggerKey {
|
||||
@ -367,7 +459,11 @@ class HoverTriggerKey {
|
||||
other is HoverTriggerKey &&
|
||||
runtimeType == other.runtimeType &&
|
||||
nodeId == other.nodeId &&
|
||||
selection == other.selection;
|
||||
isSelectionSame(other.selection);
|
||||
|
||||
bool isSelectionSame(Selection other) =>
|
||||
(selection.start == other.start && selection.end == other.end) ||
|
||||
(selection.start == other.end && selection.end == other.start);
|
||||
|
||||
@override
|
||||
int get hashCode => nodeId.hashCode ^ selection.hashCode;
|
||||
|
||||
@ -12,6 +12,8 @@ import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:appflowy_editor/src/editor/util/link_util.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -25,12 +27,16 @@ class LinkSearchTextField {
|
||||
this.onEscape,
|
||||
this.onEnter,
|
||||
this.onDataRefresh,
|
||||
this.initialViewId = '',
|
||||
required this.currentViewId,
|
||||
String? initialSearchText,
|
||||
}) : textEditingController = TextEditingController(
|
||||
text: initialSearchText ?? '',
|
||||
text: isUri(initialSearchText ?? '') ? initialSearchText : '',
|
||||
);
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
final String initialViewId;
|
||||
final String currentViewId;
|
||||
final ItemScrollController searchController = ItemScrollController();
|
||||
late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent);
|
||||
final List<ViewPB> searchedViews = [];
|
||||
@ -43,7 +49,7 @@ class LinkSearchTextField {
|
||||
|
||||
String get searchText => textEditingController.text;
|
||||
|
||||
bool get isButtonEnable => searchText.isNotEmpty;
|
||||
bool get isTextfieldEnable => searchText.isNotEmpty && isUri(searchText);
|
||||
|
||||
bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty;
|
||||
|
||||
@ -58,7 +64,11 @@ class LinkSearchTextField {
|
||||
recentViews.clear();
|
||||
}
|
||||
|
||||
Widget buildTextField({bool autofocus = false}) {
|
||||
Widget buildTextField({
|
||||
bool autofocus = false,
|
||||
bool showError = false,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
return TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
autofocus: autofocus,
|
||||
@ -81,6 +91,8 @@ class LinkSearchTextField {
|
||||
},
|
||||
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
|
||||
LocaleKeys.document_toolbar_linkInputHint.tr(),
|
||||
context,
|
||||
showErrorBorder: showError,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -177,29 +189,41 @@ class LinkSearchTextField {
|
||||
bool isSelected,
|
||||
ValueChanged<ViewPB>? onSubmittedPageLink,
|
||||
) {
|
||||
final viewName = view.name;
|
||||
final displayName = viewName.isEmpty
|
||||
? LocaleKeys.document_title_placeholder.tr()
|
||||
: viewName;
|
||||
final isCurrent = initialViewId == view.id;
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
isSelected: isSelected,
|
||||
leftIcon: buildIcon(view),
|
||||
leftIcon: buildIcon(view, padding: EdgeInsets.zero),
|
||||
text: FlowyText.regular(
|
||||
view.name,
|
||||
displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
),
|
||||
rightIcon: isCurrent ? FlowySvg(FlowySvgs.toolbar_check_m) : null,
|
||||
onTap: () => onSubmittedPageLink?.call(view),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildIcon(ViewPB view) {
|
||||
Widget buildIcon(
|
||||
ViewPB view, {
|
||||
EdgeInsetsGeometry padding = const EdgeInsets.only(top: 4),
|
||||
}) {
|
||||
if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20));
|
||||
final iconData = view.icon.toEmojiIconData();
|
||||
return RawEmojiIconWidget(
|
||||
emoji: iconData,
|
||||
emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20,
|
||||
lineHeight: 1,
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: RawEmojiIconWidget(
|
||||
emoji: iconData,
|
||||
emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20,
|
||||
lineHeight: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -288,6 +312,7 @@ class LinkSearchTextField {
|
||||
final views = sectionViews
|
||||
.unique((e) => e.item.id)
|
||||
.map((e) => e.item)
|
||||
.where((e) => e.id != currentViewId)
|
||||
.take(5)
|
||||
.toList();
|
||||
recentViews.clear();
|
||||
@ -303,13 +328,14 @@ class LinkSearchTextField {
|
||||
?.items
|
||||
.where(
|
||||
(view) =>
|
||||
view.name.toLowerCase().contains(search.toLowerCase()) ||
|
||||
(view.name.isEmpty && search.isEmpty) ||
|
||||
(view.name.isEmpty &&
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName
|
||||
.tr()
|
||||
.toLowerCase()
|
||||
.contains(search.toLowerCase())),
|
||||
(view.id != currentViewId) &&
|
||||
(view.name.toLowerCase().contains(search.toLowerCase()) ||
|
||||
(view.name.isEmpty && search.isEmpty) ||
|
||||
(view.name.isEmpty &&
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName
|
||||
.tr()
|
||||
.toLowerCase()
|
||||
.contains(search.toLowerCase()))),
|
||||
)
|
||||
.take(10)
|
||||
.toList();
|
||||
|
||||
@ -1,19 +1,33 @@
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LinkStyle {
|
||||
static const borderColor = Color(0xFFE8ECF3);
|
||||
static const textTertiary = Color(0xFF99A1A8);
|
||||
static const textStatusError = Color(0xffE71D32);
|
||||
static const fillThemeThick = Color(0xFF00B5FF);
|
||||
static const shadowMedium = Color(0x1F22251F);
|
||||
static const textPrimary = Color(0xFF1F2329);
|
||||
|
||||
static InputDecoration buildLinkTextFieldInputDecoration(String hintText) {
|
||||
const border = OutlineInputBorder(
|
||||
static Color borderColor(BuildContext context) =>
|
||||
Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0xffbdbdbd);
|
||||
|
||||
static InputDecoration buildLinkTextFieldInputDecoration(
|
||||
String hintText,
|
||||
BuildContext context, {
|
||||
bool showErrorBorder = false,
|
||||
}) {
|
||||
final border = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
borderSide: BorderSide(color: LinkStyle.borderColor),
|
||||
borderSide: BorderSide(
|
||||
color: borderColor(context),
|
||||
),
|
||||
);
|
||||
final enableBorder = border.copyWith(
|
||||
borderSide: BorderSide(color: LinkStyle.fillThemeThick),
|
||||
borderSide: BorderSide(
|
||||
color: showErrorBorder
|
||||
? LinkStyle.textStatusError
|
||||
: LinkStyle.fillThemeThick,
|
||||
),
|
||||
);
|
||||
const hintStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
|
||||
class ToolbarCubit extends Cubit<ToolbarState> {
|
||||
ToolbarCubit(this.onDismissCallback) : super(ToolbarState._());
|
||||
|
||||
final VoidCallback onDismissCallback;
|
||||
|
||||
void dismiss() {
|
||||
onDismissCallback.call();
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarState {
|
||||
const ToolbarState._();
|
||||
}
|
||||
@ -2,6 +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_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart';
|
||||
@ -68,7 +69,7 @@ class _FormatToolbarItem extends ToolbarItem {
|
||||
final hoverColor = isHighlight
|
||||
? highlightColor
|
||||
: EditorStyleCustomizer.toolbarHoverColor(context);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final isDark = !Theme.of(context).isLightMode;
|
||||
|
||||
final child = FlowyIconButton(
|
||||
width: 36,
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker;
|
||||
import 'package:flowy_infra/theme_extension_v2.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -78,7 +79,7 @@ class _HighlightColorPickerWidgetState
|
||||
}
|
||||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final iconColor = Theme.of(context).iconTheme.color;
|
||||
final iconColor = AFThemeExtensionV2.of(context).icon_primary;
|
||||
|
||||
final child = FlowyIconButton(
|
||||
width: 36,
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.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/util/theme_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension_v2.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'toolbar_id_enum.dart';
|
||||
|
||||
const kIsPageLink = 'is_page_link';
|
||||
@ -28,10 +30,10 @@ final customLinkItem = ToolbarItem(
|
||||
);
|
||||
});
|
||||
|
||||
final isDark = !Theme.of(context).isLightMode;
|
||||
final hoverColor = isHref
|
||||
? highlightColor
|
||||
: EditorStyleCustomizer.toolbarHoverColor(context);
|
||||
final toolbarCubit = context.read<ToolbarCubit?>();
|
||||
|
||||
final child = FlowyIconButton(
|
||||
width: 36,
|
||||
@ -41,16 +43,20 @@ final customLinkItem = ToolbarItem(
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.toolbar_link_m,
|
||||
size: Size.square(20.0),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
color: (isDark && isHref)
|
||||
? Color(0xFF282E3A)
|
||||
: AFThemeExtensionV2.of(context).icon_primary,
|
||||
),
|
||||
onPressed: () {
|
||||
toolbarCubit?.dismiss();
|
||||
if (isHref) {
|
||||
getIt<LinkHoverTriggers>().call(
|
||||
HoverTriggerKey(nodes.first.id, selection),
|
||||
);
|
||||
getIt<FloatingToolbarController>().hideToolbar();
|
||||
if (!isHref) {
|
||||
final viewId = context.read<DocumentBloc?>()?.documentId ?? '';
|
||||
showLinkCreateMenu(context, editorState, selection, viewId);
|
||||
} else {
|
||||
showLinkCreateMenu(context, editorState, selection);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
getIt<LinkHoverTriggers>()
|
||||
.call(HoverTriggerKey(nodes.first.id, selection));
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_to
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker;
|
||||
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 'toolbar_id_enum.dart';
|
||||
@ -77,7 +78,7 @@ class _TextColorPickerWidgetState extends State<TextColorPickerWidget> {
|
||||
}
|
||||
|
||||
Widget buildChild(BuildContext context) {
|
||||
final iconColor = Theme.of(context).iconTheme.color;
|
||||
final iconColor = AFThemeExtensionV2.of(context).icon_primary;
|
||||
final child = FlowyIconButton(
|
||||
width: 36,
|
||||
height: 32,
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.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';
|
||||
@ -288,7 +289,7 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
|
||||
popoverController: suggestionsPopoverController,
|
||||
popoverDirection: PopoverDirection.leftWithTopAligned,
|
||||
showOffset: Offset(-8, height),
|
||||
onSelect: () => context.read<ToolbarCubit?>()?.dismiss(),
|
||||
onSelect: () => getIt<FloatingToolbarController>().hideToolbar(),
|
||||
child: buildCommandItem(
|
||||
MoreOptionCommand.suggestions,
|
||||
rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m),
|
||||
@ -308,7 +309,7 @@ class _MoreOptionActionListState extends State<MoreOptionActionList> {
|
||||
popoverController: textAlignPopoverController,
|
||||
popoverDirection: PopoverDirection.leftWithTopAligned,
|
||||
showOffset: Offset(-8, 0),
|
||||
onSelect: () => context.read<ToolbarCubit?>()?.dismiss(),
|
||||
onSelect: () => getIt<FloatingToolbarController>().hideToolbar(),
|
||||
highlightColor: highlightColor,
|
||||
child: buildCommandItem(
|
||||
MoreOptionCommand.textAlign,
|
||||
@ -379,13 +380,14 @@ enum MoreOptionCommand {
|
||||
(attributes) => attributes[AppFlowyRichTextKeys.href] != null,
|
||||
);
|
||||
});
|
||||
context.read<ToolbarCubit?>()?.dismiss();
|
||||
getIt<FloatingToolbarController>().hideToolbar();
|
||||
if (isHref) {
|
||||
getIt<LinkHoverTriggers>().call(
|
||||
HoverTriggerKey(nodes.first.id, selection),
|
||||
);
|
||||
} else {
|
||||
showLinkCreateMenu(context, editorState, selection);
|
||||
final viewId = context.read<DocumentBloc?>()?.documentId ?? '';
|
||||
showLinkCreateMenu(context, editorState, selection, viewId);
|
||||
}
|
||||
} else if (this == strikethrough) {
|
||||
await editorState.toggleAttribute(name);
|
||||
|
||||
@ -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/desktop_floating_toolbar.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';
|
||||
@ -187,6 +188,9 @@ Future<void> initGetIt(
|
||||
getIt.registerSingleton<PluginSandbox>(PluginSandbox());
|
||||
getIt.registerSingleton<ViewExpanderRegistry>(ViewExpanderRegistry());
|
||||
getIt.registerSingleton<LinkHoverTriggers>(LinkHoverTriggers());
|
||||
getIt.registerSingleton<FloatingToolbarController>(
|
||||
FloatingToolbarController(),
|
||||
);
|
||||
|
||||
await DependencyResolver.resolve(getIt, mode);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user