refactor: cover node widget code (#2899)

* chore: change initial cover type's name to none

* chore: refactor cover node widget

* chore: use a constant instead of magic value

* fix: make the size of icon hover effect smaller

* chore: improve appearance of selected color

* test: add cover integration tests

* fix: inner ring of selected color in dark mode

* refactor: cover node to document header node

* test: simplify tests

* chore: rename files
This commit is contained in:
Richard Shiue 2023-06-27 15:17:51 +08:00 committed by GitHub
parent a3e09f6021
commit 7f74fd6149
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 865 additions and 672 deletions

View File

@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/emoji.dart';
import 'util/ime.dart';
import 'util/util.dart';

View File

@ -1,6 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/emoji.dart';
import '../util/util.dart';
void main() {
@ -22,15 +24,109 @@ void main() {
await TestFolder.cleanTestLocation(null);
});
testWidgets(
'hovering on cover image will display change and delete cover image buttons',
(tester) async {
testWidgets('document cover tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.hoverOnCoverPluginAddButton();
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document cover
await tester.editor.tapOnAddCover();
tester.expectToSeeDocumentCover(
CoverType.asset,
"assets/images/app_flowy_abstract_cover_1.jpg",
);
// Hover over the cover to show the 'Change Cover' and delete buttons
await tester.editor.hoverOnCover();
tester.expectChangeCoverAndDeleteButton();
// Change cover to a solid color background
await tester.editor.hoverOnCover();
await tester.editor.tapOnChangeCover();
await tester.editor.switchSolidColorBackground();
await tester.editor.dismissCoverPicker();
tester.expectToSeeDocumentCover(CoverType.color, "ffe8e0ff");
// Remove the cover
await tester.editor.hoverOnCover();
await tester.editor.tapOnRemoveCover();
tester.expectToSeeNoDocumentCover();
});
testWidgets('document icon tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Remove the document icon from the cover toolbar
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapRemoveIconButton();
tester.expectToSeeDocumentIcon(null);
// Add the icon back for further testing
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Change the document icon
await tester.editor.tapOnIconWidget();
await tester.switchToEmojiList();
await tester.tapEmoji('😅');
tester.expectToSeeDocumentIcon('😅');
// Remove the document icon from the icon picker
await tester.editor.tapOnIconWidget();
await tester.editor.tapRemoveIconButton(isInPicker: true);
tester.expectToSeeDocumentIcon(null);
});
testWidgets('icon and cover at the same time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Insert a document cover
await tester.editor.tapOnAddCover();
// Expect to see the icon and cover at the same time
tester.expectToSeeDocumentIcon('😀');
tester.expectToSeeDocumentCover(
CoverType.asset,
"assets/images/app_flowy_abstract_cover_1.jpg",
);
// Hover over the cover toolbar and see that neither icons are shown
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeeEmptyDocumentHeaderToolbar();
});
});
}

View File

@ -461,17 +461,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(EmojiSelectionMenu));
}
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
}
Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
await tapButton(emojiWidget);
}
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(GridDateCell);
await tapButton(findDateCell);

View File

@ -1,4 +1,13 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'ime.dart';
@ -26,14 +35,78 @@ class EditorOperations {
}
/// Hover on cover plugin button above the document
Future<void> hoverOnCoverPluginAddButton() async {
final editor = find.byWidgetPredicate(
(widget) => widget is AppFlowyEditor,
Future<void> hoverOnCoverToolbar() async {
final coverToolbar = find.byType(DocumentHeaderToolbar);
await tester.startGesture(
tester.getBottomLeft(coverToolbar).translate(5, -5),
kind: PointerDeviceKind.mouse,
);
await tester.hoverOnWidget(
editor,
offset: tester.getTopLeft(editor).translate(20, 20),
await tester.pumpAndSettle();
}
/// Taps on the 'Add Icon' button in the cover toolbar
Future<void> tapAddIconButton() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(find.byType(EmojiPopover), findsOneWidget);
}
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
Finder button =
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
if (isInPicker) {
button = find.descendant(of: find.byType(EmojiPopover), matching: button);
}
await tester.tapButton(button);
}
/// Requires that the document must already have an icon. This opens the icon
/// picker
Future<void> tapOnIconWidget() async {
final iconWidget = find.byType(EmojiIconWidget);
await tester.tapButton(iconWidget);
}
Future<void> tapOnAddCover() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
}
Future<void> tapOnChangeCover() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
}
Future<void> switchSolidColorBackground() async {
final findPurpleButton = find.byWidgetPredicate(
(widget) => widget is ColorItem && widget.option.colorHex == "ffe8e0ff",
);
await tester.tapButton(findPurpleButton);
}
Future<void> tapOnRemoveCover() async {
await tester.tapButton(find.byType(DeleteCoverButton));
}
/// A cover must be present in the document to function properly since this
/// catches all cover types collectively
Future<void> hoverOnCover() async {
final cover = find.byType(DocumentCover);
await tester.startGesture(
tester.getCenter(cover),
kind: PointerDeviceKind.mouse,
);
await tester.pumpAndSettle();
}
Future<void> dismissCoverPicker() async {
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
}
/// trigger the slash command (selection menu)

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
extension EmojiTestExtension on WidgetTester {
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
}
Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
await tapButton(emojiWidget);
}
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:easy_localization/easy_localization.dart';
@ -47,7 +49,7 @@ extension Expectation on WidgetTester {
expect(exportSuccess, findsOneWidget);
}
/// Expect to see the add button and icon button inside the document.
/// Expect to see the add button and icon button in the cover toolbar
void expectToSeePluginAddCoverAndIconButton() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
@ -59,6 +61,54 @@ extension Expectation on WidgetTester {
expect(addIcon, findsOneWidget);
}
/// Expect to see the document header toolbar empty
void expectToSeeEmptyDocumentHeaderToolbar() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
final addIcon = find.textContaining(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(addCover, findsNothing);
expect(addIcon, findsNothing);
}
void expectToSeeDocumentIcon(String? emoji) {
if (emoji == null) {
final iconWidget = find.byType(EmojiIconWidget);
expect(iconWidget, findsNothing);
return;
}
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
);
expect(iconWidget, findsOneWidget);
}
void expectToSeeDocumentCover(CoverType type, String details) {
final findCover = find.byWidgetPredicate(
(widget) =>
widget is DocumentCover &&
widget.coverType == type &&
widget.coverDetails == details,
);
expect(findCover, findsOneWidget);
}
void expectToSeeNoDocumentCover() {
final findCover = find.byType(DocumentCover);
expect(findCover, findsNothing);
}
void expectChangeCoverAndDeleteButton() {
final findChangeCover = find.text(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
final findRemoveIcon = find.byType(DeleteCoverButton);
expect(findChangeCover, findsOneWidget);
expect(findRemoveIcon, findsOneWidget);
}
/// Expect to see the user name on the home page
void expectToSeeUserName(String name) {
final userName = find.byWidgetPredicate(

View File

@ -123,7 +123,7 @@ class _DocumentPageState extends State<DocumentPage> {
return const Placeholder();
}
final page = editorState!.document.root;
return CoverImageNodeWidget(
return DocumentHeaderNodeWidget(
node: page,
editorState: editorState!,
);

View File

@ -1,555 +0,0 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class CoverBlockKeys {
const CoverBlockKeys._();
static const String selectionType = 'cover_selection_type';
static const String selection = 'cover_selection';
static const String iconSelection = 'selected_icon';
}
enum CoverSelectionType {
initial,
color,
file,
asset;
static CoverSelectionType fromString(String? value) {
if (value == null) {
return CoverSelectionType.initial;
}
return CoverSelectionType.values.firstWhere(
(e) => e.toString() == value,
orElse: () => CoverSelectionType.initial,
);
}
}
class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
return CoverImageNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return true;
};
}
class CoverImageNodeWidget extends StatefulWidget {
const CoverImageNodeWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
}
class _CoverImageNodeWidgetState extends State<CoverImageNodeWidget> {
CoverSelectionType get selectionType => CoverSelectionType.fromString(
widget.node.attributes[CoverBlockKeys.selectionType],
);
@override
void initState() {
super.initState();
widget.node.addListener(_reload);
}
@override
void dispose() {
widget.node.removeListener(_reload);
super.dispose();
}
void _reload() {
setState(() {});
}
PopoverController iconPopoverController = PopoverController();
@override
Widget build(BuildContext context) {
return _CoverImage(
editorState: widget.editorState,
node: widget.node,
onCoverChanged: (type, value) {
_insertCover(type, value);
},
);
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType: type.toString(),
CoverBlockKeys.selection: cover,
CoverBlockKeys.iconSelection:
widget.node.attributes[CoverBlockKeys.iconSelection]
});
return widget.editorState.apply(transaction);
}
}
class _AddCoverButton extends StatefulWidget {
final Node node;
final EditorState editorState;
final bool hasIcon;
final CoverSelectionType selectionType;
final PopoverController iconPopoverController;
const _AddCoverButton({
required this.onTap,
required this.node,
required this.editorState,
required this.hasIcon,
required this.selectionType,
required this.iconPopoverController,
});
final VoidCallback onTap;
@override
State<_AddCoverButton> createState() => _AddCoverButtonState();
}
bool isPopoverOpen = false;
class _AddCoverButtonState extends State<_AddCoverButton> {
bool isHidden = true;
PopoverMutex mutex = PopoverMutex();
bool isPopoverOpen = false;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setHidden(false);
},
onExit: (event) {
setHidden(isPopoverOpen ? false : true);
},
opaque: false,
child: Container(
height: widget.hasIcon ? 180 : 50.0,
alignment: Alignment.bottomLeft,
width: double.infinity,
padding: const EdgeInsets.only(
left: 80,
top: 20,
bottom: 5,
),
child: isHidden
? Container()
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Add Cover Button.
widget.selectionType != CoverSelectionType.initial
? Container()
: FlowyButton(
key: UniqueKey(),
leftIconSize: const Size.square(18),
onTap: widget.onTap,
useIntrinsicWidth: true,
leftIcon: const FlowySvg(name: 'editor/image'),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
),
// Add Icon Button.
widget.hasIcon
? FlowyButton(
leftIconSize: const Size.square(18),
onTap: () {
_removeIcon();
},
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
)
: AppFlowyPopover(
mutex: mutex,
asBarrier: true,
onClose: () {
isPopoverOpen = false;
setHidden(true);
},
offset: const Offset(120, 10),
controller: widget.iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints:
BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: FlowyButton(
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return EmojiPopover(
showRemoveButton: widget.hasIcon,
removeIcon: _removeIcon,
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
widget.iconPopoverController.close();
},
);
},
)
],
),
),
);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
CoverBlockKeys.iconSelection: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.iconSelection: "",
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
});
return widget.editorState.apply(transaction);
}
void setHidden(bool value) {
if (isHidden == value) return;
setState(() {
isHidden = value;
});
}
}
class _CoverImage extends StatefulWidget {
const _CoverImage({
required this.editorState,
required this.node,
required this.onCoverChanged,
});
final Node node;
final EditorState editorState;
final Function(
CoverSelectionType selectionType,
dynamic selection,
) onCoverChanged;
@override
State<_CoverImage> createState() => _CoverImageState();
}
class _CoverImageState extends State<_CoverImage> {
final popoverController = PopoverController();
CoverSelectionType get selectionType => CoverSelectionType.fromString(
widget.node.attributes[CoverBlockKeys.selectionType],
);
Color get color {
final hex = widget.node.attributes[CoverBlockKeys.selection] as String?;
return hex?.toColor() ?? Colors.white;
}
bool get hasIcon =>
widget.node.attributes[CoverBlockKeys.iconSelection] == null
? false
: widget.node.attributes[CoverBlockKeys.iconSelection].isNotEmpty;
bool isOverlayButtonsHidden = true;
PopoverController iconPopoverController = PopoverController();
bool get hasCover =>
selectionType == CoverSelectionType.initial ? false : true;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.bottomLeft,
children: [
Container(
alignment: Alignment.topCenter,
height: !hasCover
? 0
: hasIcon
? 320
: 280,
child: _buildCoverImage(context, widget.editorState),
),
hasIcon
? Positioned(
left: 80,
bottom: !hasCover ? 30 : 40,
child: AppFlowyPopover(
offset: const Offset(100, 0),
controller: iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: EmojiIconWidget(
emoji: widget.node.attributes[CoverBlockKeys.iconSelection],
),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: hasIcon,
removeIcon: _removeIcon,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
iconPopoverController.close();
},
);
},
),
)
: Container(),
hasIcon && selectionType != CoverSelectionType.initial
? Container()
: _AddCoverButton(
onTap: () {
_insertCover(
CoverSelectionType.asset,
builtInAssetImages.first,
);
},
node: widget.node,
editorState: widget.editorState,
hasIcon: hasIcon,
selectionType: selectionType,
iconPopoverController: iconPopoverController,
),
],
);
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType: type.toString(),
CoverBlockKeys.selection: cover,
CoverBlockKeys.iconSelection:
widget.node.attributes[CoverBlockKeys.iconSelection]
});
return widget.editorState.apply(transaction);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
CoverBlockKeys.iconSelection: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
CoverBlockKeys.iconSelection: "",
CoverBlockKeys.selectionType:
widget.node.attributes[CoverBlockKeys.selectionType],
CoverBlockKeys.selection:
widget.node.attributes[CoverBlockKeys.selection],
});
return widget.editorState.apply(transaction);
}
Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned(
bottom: 20,
right: 50,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppFlowyPopover(
onClose: () {
setOverlayButtonsHidden(true);
},
offset: const Offset(-125, 10),
controller: popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(380, 450)),
margin: EdgeInsets.zero,
child: Visibility(
maintainState: true,
maintainAnimation: true,
maintainSize: true,
visible: !isOverlayButtonsHidden,
child: RoundedTextButton(
onPressed: () {
popoverController.show();
setOverlayButtonsHidden(true);
},
hoverColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.tertiary,
fillColor:
Theme.of(context).colorScheme.surface.withOpacity(0.5),
width: 120,
height: 28,
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
),
),
popupBuilder: (BuildContext popoverContext) {
return ChangeCoverPopover(
node: widget.node,
editorState: widget.editorState,
onCoverChanged: widget.onCoverChanged,
);
},
),
const SizedBox(width: 10),
Visibility(
maintainAnimation: true,
maintainSize: true,
maintainState: true,
visible: !isOverlayButtonsHidden,
child: FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.surface,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
iconPadding: const EdgeInsets.all(5),
width: 28,
icon: svgWidget(
'editor/delete',
color: Theme.of(context).colorScheme.tertiary,
),
onPressed: () {
widget.onCoverChanged(CoverSelectionType.initial, null);
},
),
),
],
),
);
}
Widget _buildCoverImage(BuildContext context, EditorState editorState) {
const height = 250.0;
final Widget coverImage;
switch (selectionType) {
case CoverSelectionType.file:
final imageFile =
File(widget.node.attributes[CoverBlockKeys.selection]);
if (!imageFile.existsSync()) {
// reset cover state
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCoverChanged(CoverSelectionType.initial, null);
});
coverImage = const SizedBox();
break;
}
coverImage = Image.file(
imageFile,
fit: BoxFit.cover,
);
break;
case CoverSelectionType.asset:
coverImage = Image.asset(
widget.node.attributes[CoverBlockKeys.selection],
fit: BoxFit.cover,
);
break;
case CoverSelectionType.color:
coverImage = Container(
decoration: BoxDecoration(
color: color,
borderRadius: Corners.s6Border,
),
alignment: Alignment.center,
);
break;
case CoverSelectionType.initial:
coverImage = const SizedBox();
break;
}
// OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
return MouseRegion(
onEnter: (event) {
setOverlayButtonsHidden(false);
},
onExit: (event) {
setOverlayButtonsHidden(true);
},
child: SizedBox(
height: height,
child: Stack(
children: [
Container(
padding: const EdgeInsets.only(bottom: 10),
height: double.infinity,
width: double.infinity,
child: coverImage,
),
hasCover
? _buildCoverOverlayButtons(context)
: const SizedBox.shrink()
],
),
),
);
}
void setOverlayButtonsHidden(bool value) {
if (isOverlayButtonsHidden == value) return;
setState(() {
isOverlayButtonsHidden = value;
});
}
}

View File

@ -25,7 +25,7 @@ class ChangeCoverPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final Function(
CoverSelectionType selectionType,
CoverType selectionType,
String selection,
) onCoverChanged;
@ -149,10 +149,10 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
LocaleKeys.document_plugins_cover_clearAll.tr(),
fontColor: Theme.of(context).colorScheme.tertiary,
onPressed: () async {
final hasFileImageCover = CoverSelectionType.fromString(
widget.node.attributes[CoverBlockKeys.selectionType],
final hasFileImageCover = CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
) ==
CoverSelectionType.file;
CoverType.file;
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
if (hasFileImageCover) {
await showDialog(
@ -196,7 +196,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
return InkWell(
onTap: () {
widget.onCoverChanged(
CoverSelectionType.asset,
CoverType.asset,
builtInAssetImages[index],
);
},
@ -220,14 +220,14 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
pickerBackgroundColor: theme.cardColor,
pickerItemHoverColor: theme.hoverColor,
selectedBackgroundColorHex:
widget.node.attributes[CoverBlockKeys.selectionType] ==
CoverSelectionType.color.toString()
? widget.node.attributes[CoverBlockKeys.selection]
widget.node.attributes[DocumentHeaderBlockKeys.coverType] ==
CoverType.color.toString()
? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]
: 'ffffff',
backgroundColorOptions:
_generateBackgroundColorOptions(widget.editorState),
onSubmittedBackgroundColorHex: (color) {
widget.onCoverChanged(CoverSelectionType.color, color);
widget.onCoverChanged(CoverType.color, color);
setState(() {});
},
);
@ -276,16 +276,16 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
return ImageGridItem(
onImageSelect: () {
widget.onCoverChanged(
CoverSelectionType.file,
CoverType.file,
images[index - 1],
);
},
onImageDelete: () async {
final changeCoverBloc =
context.read<ChangeCoverPopoverBloc>();
final deletingCurrentCover =
widget.node.attributes[CoverBlockKeys.selection] ==
images[index - 1];
final deletingCurrentCover = widget.node
.attributes[DocumentHeaderBlockKeys.coverDetails] ==
images[index - 1];
if (deletingCurrentCover) {
await showDialog(
context: context,
@ -481,36 +481,63 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
scrollController.dispose();
}
Widget _buildColorItem(ColorOption option, bool isChecked) {
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: options
.map(
(e) => ColorItem(
option: e,
isChecked: e.colorHex == selectedColor,
hoverColor: widget.pickerItemHoverColor,
onTap: widget.onSubmittedBackgroundColorHex,
),
)
.toList(),
);
}
}
@visibleForTesting
class ColorItem extends StatelessWidget {
final ColorOption option;
final bool isChecked;
final Color hoverColor;
final void Function(String) onTap;
const ColorItem({
required this.option,
required this.isChecked,
required this.hoverColor,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return InkWell(
customBorder: const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
hoverColor: widget.pickerItemHoverColor,
onTap: () {
widget.onSubmittedBackgroundColorHex(option.colorHex);
},
hoverColor: hoverColor,
onTap: () => onTap(option.colorHex),
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: SizedBox.square(
dimension: isChecked ? 24 : 25,
dimension: 25,
child: Container(
decoration: BoxDecoration(
color: option.colorHex.toColor(),
border: isChecked
? Border.all(
color: const Color(0xFFFFFFFF),
width: 2.0,
)
: null,
shape: BoxShape.circle,
),
child: isChecked
? SizedBox.square(
dimension: 24,
child: Container(
margin: const EdgeInsets.all(4),
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).cardColor,
width: 3.0,
),
color: option.colorHex.toColor(),
shape: BoxShape.circle,
),
@ -522,14 +549,4 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
),
);
}
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: options
.map((e) => _buildColorItem(e, e.colorHex == selectedColor))
.toList(),
);
}
}

View File

@ -1,14 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'change_cover_popover_bloc.freezed.dart';
part 'cover_editor_bloc.freezed.dart';
class ChangeCoverPopoverBloc
extends Bloc<ChangeCoverPopoverEvent, ChangeCoverPopoverState> {
@ -32,7 +32,7 @@ class ChangeCoverPopoverBloc
deleteImage: (DeleteImage deleteImage) async {
final currentState = state;
final currentlySelectedImage =
node.attributes[CoverBlockKeys.selection];
node.attributes[DocumentHeaderBlockKeys.coverDetails];
if (currentState is Loaded) {
await _deleteImageInStorage(deleteImage.path);
if (currentlySelectedImage == deleteImage.path) {
@ -48,7 +48,7 @@ class ChangeCoverPopoverBloc
clearAllImages: (ClearAllImages clearAllImages) async {
final currentState = state;
final currentlySelectedImage =
node.attributes[CoverBlockKeys.selection];
node.attributes[DocumentHeaderBlockKeys.coverDetails];
if (currentState is Loaded) {
for (final image in currentState.imageNames) {
@ -90,9 +90,9 @@ class ChangeCoverPopoverBloc
Future<void> _removeCoverImageFromNode() async {
final transaction = editorState.transaction;
transaction.updateNode(node, {
CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(),
CoverBlockKeys.iconSelection:
node.attributes[CoverBlockKeys.iconSelection]
DocumentHeaderBlockKeys.coverType: CoverType.none.toString(),
DocumentHeaderBlockKeys.icon:
node.attributes[DocumentHeaderBlockKeys.icon]
});
return editorState.apply(transaction);
}

View File

@ -1,6 +1,6 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

View File

@ -13,9 +13,10 @@ import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as p;
import 'change_cover_popover.dart';
part 'cover_image_picker_bloc.freezed.dart';
import 'cover_editor.dart';
part 'custom_cover_picker_bloc.freezed.dart';
class CoverImagePickerBloc
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {

View File

@ -0,0 +1,507 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'cover_editor.dart';
import 'emoji_icon_widget.dart';
import 'emoji_popover.dart';
const double kCoverHeight = 250.0;
const double kIconHeight = 60.0;
const double kToolbarHeight = 40.0; // with padding to the top
class DocumentHeaderBlockKeys {
const DocumentHeaderBlockKeys._();
static const String coverType = 'cover_selection_type';
static const String coverDetails = 'cover_selection';
static const String icon = 'selected_icon';
}
enum CoverType {
none,
color,
file,
asset;
static CoverType fromString(String? value) {
if (value == null) {
return CoverType.none;
}
return CoverType.values.firstWhere(
(e) => e.toString() == value,
orElse: () => CoverType.none,
);
}
}
class DocumentHeaderNodeWidgetBuilder implements NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
return DocumentHeaderNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (_) => true;
}
class DocumentHeaderNodeWidget extends StatefulWidget {
const DocumentHeaderNodeWidget({
required this.node,
required this.editorState,
super.key,
});
final Node node;
final EditorState editorState;
@override
State<DocumentHeaderNodeWidget> createState() =>
_DocumentHeaderNodeWidgetState();
}
class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
CoverType get coverType => CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
);
String? get coverDetails =>
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];
String get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon];
bool get hasIcon =>
widget.node.attributes[DocumentHeaderBlockKeys.icon]?.isNotEmpty ?? false;
bool get hasCover => coverType != CoverType.none;
@override
void initState() {
super.initState();
widget.node.addListener(_reload);
}
@override
void dispose() {
widget.node.removeListener(_reload);
super.dispose();
}
void _reload() => setState(() {});
@override
Widget build(BuildContext context) {
return Stack(
children: [
SizedBox(
height: _calculateOverallHeight(),
child: DocumentHeaderToolbar(
onCoverChanged: _saveCover,
node: widget.node,
editorState: widget.editorState,
hasCover: hasCover,
hasIcon: hasIcon,
),
),
if (hasCover)
DocumentCover(
editorState: widget.editorState,
node: widget.node,
coverType: coverType,
coverDetails: coverDetails,
onCoverChanged: (type, details) =>
_saveCover(cover: (type, details)),
),
if (hasIcon)
Positioned(
left: 80,
// if hasCover, there shouldn't be icons present so the icon can
// be closer to the bottom.
bottom:
hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight,
child: DocumentIcon(
editorState: widget.editorState,
node: widget.node,
icon: icon,
onIconChanged: (icon) => _saveCover(icon: icon),
),
),
],
);
}
double _calculateOverallHeight() {
switch ((hasIcon, hasCover)) {
case (true, true):
return kCoverHeight + kToolbarHeight;
case (true, false):
return 50 + kIconHeight + kToolbarHeight;
case (false, true):
return kCoverHeight + kToolbarHeight;
case (false, false):
return kToolbarHeight;
}
}
Future<void> _saveCover({(CoverType, String?)? cover, String? icon}) {
final transaction = widget.editorState.transaction;
final Map<String, dynamic> attributes = {
DocumentHeaderBlockKeys.coverType:
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
DocumentHeaderBlockKeys.coverDetails:
widget.node.attributes[DocumentHeaderBlockKeys.coverDetails],
DocumentHeaderBlockKeys.icon:
widget.node.attributes[DocumentHeaderBlockKeys.icon]
};
if (cover != null) {
attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();
attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2;
}
if (icon != null) {
attributes[DocumentHeaderBlockKeys.icon] = icon;
}
transaction.updateNode(widget.node, attributes);
return widget.editorState.apply(transaction);
}
}
@visibleForTesting
class DocumentHeaderToolbar extends StatefulWidget {
final Node node;
final EditorState editorState;
final bool hasCover;
final bool hasIcon;
final Future<void> Function({(CoverType, String?)? cover, String? icon})
onCoverChanged;
const DocumentHeaderToolbar({
required this.node,
required this.editorState,
required this.hasCover,
required this.hasIcon,
required this.onCoverChanged,
super.key,
});
@override
State<DocumentHeaderToolbar> createState() => _DocumentHeaderToolbarState();
}
class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
bool isHidden = true;
bool isPopoverOpen = false;
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) => setHidden(false),
onExit: (event) {
if (!isPopoverOpen) {
setHidden(true);
}
},
opaque: false,
child: Container(
alignment: Alignment.bottomLeft,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 80),
child: SizedBox(
height: 28,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: buildRowChildren(),
),
),
),
);
}
List<Widget> buildRowChildren() {
if (isHidden || widget.hasCover && widget.hasIcon) {
return [];
}
final List<Widget> children = [];
if (!widget.hasCover) {
children.add(
FlowyButton(
leftIconSize: const Size.square(18),
onTap: () => widget.onCoverChanged(
cover: (CoverType.asset, builtInAssetImages.first),
),
useIntrinsicWidth: true,
leftIcon: const FlowySvg(name: 'editor/image'),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
),
);
}
if (widget.hasIcon) {
children.add(
FlowyButton(
leftIconSize: const Size.square(18),
onTap: () => widget.onCoverChanged(icon: ""),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
),
);
} else {
children.add(
AppFlowyPopover(
onClose: () => isPopoverOpen = false,
controller: _popoverController,
offset: const Offset(0, 8),
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(320, 380)),
child: FlowyButton(
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: const Icon(
Icons.emoji_emotions_outlined,
size: 18,
),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return EmojiPopover(
showRemoveButton: widget.hasIcon,
removeIcon: () {
widget.onCoverChanged(icon: "");
_popoverController.close();
},
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
widget.onCoverChanged(icon: emoji.emoji);
_popoverController.close();
},
);
},
),
);
}
return children;
}
void setHidden(bool value) {
if (isHidden == value) return;
setState(() {
isHidden = value;
});
}
}
@visibleForTesting
class DocumentCover extends StatefulWidget {
final Node node;
final EditorState editorState;
final CoverType coverType;
final String? coverDetails;
final Future<void> Function(CoverType type, String? details) onCoverChanged;
const DocumentCover({
required this.editorState,
required this.node,
required this.coverType,
required this.onCoverChanged,
this.coverDetails,
super.key,
});
@override
State<DocumentCover> createState() => DocumentCoverState();
}
class DocumentCoverState extends State<DocumentCover> {
bool isOverlayButtonsHidden = true;
bool isPopoverOpen = false;
@override
Widget build(BuildContext context) {
return SizedBox(
height: kCoverHeight,
child: MouseRegion(
onEnter: (event) => setOverlayButtonsHidden(false),
onExit: (event) =>
setOverlayButtonsHidden(isPopoverOpen ? false : true),
child: Stack(
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: _buildCoverImage(),
),
if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context)
],
),
),
);
}
Widget _buildCoverImage() {
switch (widget.coverType) {
case CoverType.file:
final imageFile = File(widget.coverDetails ?? "");
if (!imageFile.existsSync()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCoverChanged(CoverType.none, null);
});
return const SizedBox.shrink();
}
return Image.file(
imageFile,
fit: BoxFit.cover,
);
case CoverType.asset:
return Image.asset(
widget.coverDetails!,
fit: BoxFit.cover,
);
case CoverType.color:
final color = widget.coverDetails?.toColor() ?? Colors.white;
return Container(color: color);
case CoverType.none:
return const SizedBox.shrink();
}
}
Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned(
bottom: 20,
right: 50,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppFlowyPopover(
offset: const Offset(0, 8),
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(380, 450)),
margin: EdgeInsets.zero,
onClose: () => isPopoverOpen = false,
child: RoundedTextButton(
hoverColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.tertiary,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
width: 120,
height: 28,
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return ChangeCoverPopover(
node: widget.node,
editorState: widget.editorState,
onCoverChanged: (cover, selection) =>
widget.onCoverChanged(cover, selection),
);
},
),
const HSpace(10),
DeleteCoverButton(
onTap: () => widget.onCoverChanged(CoverType.none, null),
),
],
),
);
}
void setOverlayButtonsHidden(bool value) {
if (isOverlayButtonsHidden == value) return;
setState(() {
isOverlayButtonsHidden = value;
});
}
}
@visibleForTesting
class DeleteCoverButton extends StatelessWidget {
final VoidCallback onTap;
const DeleteCoverButton({required this.onTap, super.key});
@override
Widget build(BuildContext context) {
return FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.surface,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
iconPadding: const EdgeInsets.all(5),
width: 28,
icon: svgWidget(
'editor/delete',
color: Theme.of(context).colorScheme.tertiary,
),
onPressed: onTap,
);
}
}
@visibleForTesting
class DocumentIcon extends StatefulWidget {
final Node node;
final EditorState editorState;
final String icon;
final Future<void> Function(String icon) onIconChanged;
const DocumentIcon({
required this.node,
required this.editorState,
required this.icon,
required this.onIconChanged,
super.key,
});
@override
State<DocumentIcon> createState() => _DocumentIconState();
}
class _DocumentIconState extends State<DocumentIcon> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: BoxConstraints.loose(const Size(320, 380)),
child: EmojiIconWidget(emoji: widget.icon),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: true,
removeIcon: () {
widget.onIconChanged("");
_popoverController.close();
},
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
widget.onIconChanged(emoji.emoji);
_popoverController.close();
},
);
},
);
}
}

View File

@ -5,12 +5,10 @@ class EmojiIconWidget extends StatefulWidget {
const EmojiIconWidget({
super.key,
required this.emoji,
this.size = 80,
this.emojiSize = 60,
});
final String emoji;
final double size;
final double emojiSize;
@override
@ -25,12 +23,11 @@ class _EmojiIconWidgetState extends State<EmojiIconWidget> {
return MouseRegion(
onEnter: (_) => setHidden(false),
onExit: (_) => setHidden(true),
cursor: SystemMouseCursors.click,
child: Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
color: !hover
? Theme.of(context).colorScheme.inverseSurface
? Theme.of(context).colorScheme.inverseSurface.withOpacity(0.5)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),

View File

@ -30,38 +30,35 @@ class EmojiPopover extends StatefulWidget {
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(15),
child: Column(
children: [
if (widget.showRemoveButton)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: DeleteButton(onTap: widget.removeIcon),
),
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
config: Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Theme.of(context).iconTheme.color!,
iconColorSelected: Theme.of(context).colorScheme.onSurface,
selectedHoverColor: Theme.of(context).colorScheme.secondary,
progressIndicatorColor: Theme.of(context).iconTheme.color!,
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
return Column(
children: [
if (widget.showRemoveButton)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: DeleteButton(onTap: widget.removeIcon),
),
),
],
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
config: Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Theme.of(context).iconTheme.color!,
iconColorSelected: Theme.of(context).colorScheme.onSurface,
selectedHoverColor: Theme.of(context).colorScheme.secondary,
progressIndicatorColor: Theme.of(context).iconTheme.color!,
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
),
],
);
}
}
@ -72,13 +69,16 @@ class DeleteButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyButton(
onTap: onTap,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
return SizedBox(
height: 28,
child: FlowyButton(
onTap: onTap,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(name: 'editor/delete'),
),
leftIcon: const FlowySvg(name: 'editor/delete'),
);
}
}

View File

@ -1,9 +1,9 @@
export 'callout/callout_block_component.dart';
export 'code_block/code_block_component.dart';
export 'code_block/code_block_shortcut_event.dart';
export 'cover/change_cover_popover_bloc.dart';
export 'cover/cover_node_widget.dart';
export 'cover/cover_image_picker.dart';
export 'header/cover_editor_bloc.dart';
export 'header/document_header_node_widget.dart';
export 'header/custom_cover_picker.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.dart';
export 'database/inline_database_menu_item.dart';

View File

@ -81,7 +81,7 @@ class FlowyIconButton extends StatelessWidget {
hoverColor: hoverColor,
foregroundColorOnHover:
iconColorOnHover ?? Theme.of(context).iconTheme.color,
backgroundColor: fillColor ?? Colors.transparent,
backgroundColor: Colors.transparent,
),
child: Padding(
padding: iconPadding,