mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'util/database_test_op.dart';
|
import 'util/database_test_op.dart';
|
||||||
|
import 'util/emoji.dart';
|
||||||
import 'util/ime.dart';
|
import 'util/ime.dart';
|
||||||
import 'util/util.dart';
|
import 'util/util.dart';
|
||||||
|
|
||||||
|
@ -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:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import '../util/emoji.dart';
|
||||||
import '../util/util.dart';
|
import '../util/util.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -22,15 +24,109 @@ void main() {
|
|||||||
await TestFolder.cleanTestLocation(null);
|
await TestFolder.cleanTestLocation(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets('document cover tests', (tester) async {
|
||||||
'hovering on cover image will display change and delete cover image buttons',
|
|
||||||
(tester) async {
|
|
||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
|
|
||||||
await tester.tapGoButton();
|
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();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -461,17 +461,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(find.byType(EmojiSelectionMenu));
|
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 {
|
Future<void> tapDateCellInRowDetailPage() async {
|
||||||
final findDateCell = find.byType(GridDateCell);
|
final findDateCell = find.byType(GridDateCell);
|
||||||
await tapButton(findDateCell);
|
await tapButton(findDateCell);
|
||||||
|
@ -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: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 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'ime.dart';
|
import 'ime.dart';
|
||||||
@ -26,14 +35,78 @@ class EditorOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Hover on cover plugin button above the document
|
/// Hover on cover plugin button above the document
|
||||||
Future<void> hoverOnCoverPluginAddButton() async {
|
Future<void> hoverOnCoverToolbar() async {
|
||||||
final editor = find.byWidgetPredicate(
|
final coverToolbar = find.byType(DocumentHeaderToolbar);
|
||||||
(widget) => widget is AppFlowyEditor,
|
await tester.startGesture(
|
||||||
|
tester.getBottomLeft(coverToolbar).translate(5, -5),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
);
|
);
|
||||||
await tester.hoverOnWidget(
|
await tester.pumpAndSettle();
|
||||||
editor,
|
}
|
||||||
offset: tester.getTopLeft(editor).translate(20, 20),
|
|
||||||
|
/// 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)
|
/// trigger the slash command (selection menu)
|
||||||
|
17
frontend/appflowy_flutter/integration_test/util/emoji.dart
Normal file
17
frontend/appflowy_flutter/integration_test/util/emoji.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/banner.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/home_stack.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -47,7 +49,7 @@ extension Expectation on WidgetTester {
|
|||||||
expect(exportSuccess, findsOneWidget);
|
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() {
|
void expectToSeePluginAddCoverAndIconButton() {
|
||||||
final addCover = find.textContaining(
|
final addCover = find.textContaining(
|
||||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||||
@ -59,6 +61,54 @@ extension Expectation on WidgetTester {
|
|||||||
expect(addIcon, findsOneWidget);
|
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
|
/// Expect to see the user name on the home page
|
||||||
void expectToSeeUserName(String name) {
|
void expectToSeeUserName(String name) {
|
||||||
final userName = find.byWidgetPredicate(
|
final userName = find.byWidgetPredicate(
|
||||||
|
@ -123,7 +123,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
return const Placeholder();
|
return const Placeholder();
|
||||||
}
|
}
|
||||||
final page = editorState!.document.root;
|
final page = editorState!.document.root;
|
||||||
return CoverImageNodeWidget(
|
return DocumentHeaderNodeWidget(
|
||||||
node: page,
|
node: page,
|
||||||
editorState: editorState!,
|
editorState: editorState!,
|
||||||
);
|
);
|
||||||
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,7 +25,7 @@ class ChangeCoverPopover extends StatefulWidget {
|
|||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final Node node;
|
final Node node;
|
||||||
final Function(
|
final Function(
|
||||||
CoverSelectionType selectionType,
|
CoverType selectionType,
|
||||||
String selection,
|
String selection,
|
||||||
) onCoverChanged;
|
) onCoverChanged;
|
||||||
|
|
||||||
@ -149,10 +149,10 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
LocaleKeys.document_plugins_cover_clearAll.tr(),
|
LocaleKeys.document_plugins_cover_clearAll.tr(),
|
||||||
fontColor: Theme.of(context).colorScheme.tertiary,
|
fontColor: Theme.of(context).colorScheme.tertiary,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final hasFileImageCover = CoverSelectionType.fromString(
|
final hasFileImageCover = CoverType.fromString(
|
||||||
widget.node.attributes[CoverBlockKeys.selectionType],
|
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
|
||||||
) ==
|
) ==
|
||||||
CoverSelectionType.file;
|
CoverType.file;
|
||||||
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
|
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
|
||||||
if (hasFileImageCover) {
|
if (hasFileImageCover) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@ -196,7 +196,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onCoverChanged(
|
widget.onCoverChanged(
|
||||||
CoverSelectionType.asset,
|
CoverType.asset,
|
||||||
builtInAssetImages[index],
|
builtInAssetImages[index],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -220,14 +220,14 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
pickerBackgroundColor: theme.cardColor,
|
pickerBackgroundColor: theme.cardColor,
|
||||||
pickerItemHoverColor: theme.hoverColor,
|
pickerItemHoverColor: theme.hoverColor,
|
||||||
selectedBackgroundColorHex:
|
selectedBackgroundColorHex:
|
||||||
widget.node.attributes[CoverBlockKeys.selectionType] ==
|
widget.node.attributes[DocumentHeaderBlockKeys.coverType] ==
|
||||||
CoverSelectionType.color.toString()
|
CoverType.color.toString()
|
||||||
? widget.node.attributes[CoverBlockKeys.selection]
|
? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]
|
||||||
: 'ffffff',
|
: 'ffffff',
|
||||||
backgroundColorOptions:
|
backgroundColorOptions:
|
||||||
_generateBackgroundColorOptions(widget.editorState),
|
_generateBackgroundColorOptions(widget.editorState),
|
||||||
onSubmittedBackgroundColorHex: (color) {
|
onSubmittedBackgroundColorHex: (color) {
|
||||||
widget.onCoverChanged(CoverSelectionType.color, color);
|
widget.onCoverChanged(CoverType.color, color);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -276,16 +276,16 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
|||||||
return ImageGridItem(
|
return ImageGridItem(
|
||||||
onImageSelect: () {
|
onImageSelect: () {
|
||||||
widget.onCoverChanged(
|
widget.onCoverChanged(
|
||||||
CoverSelectionType.file,
|
CoverType.file,
|
||||||
images[index - 1],
|
images[index - 1],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onImageDelete: () async {
|
onImageDelete: () async {
|
||||||
final changeCoverBloc =
|
final changeCoverBloc =
|
||||||
context.read<ChangeCoverPopoverBloc>();
|
context.read<ChangeCoverPopoverBloc>();
|
||||||
final deletingCurrentCover =
|
final deletingCurrentCover = widget.node
|
||||||
widget.node.attributes[CoverBlockKeys.selection] ==
|
.attributes[DocumentHeaderBlockKeys.coverDetails] ==
|
||||||
images[index - 1];
|
images[index - 1];
|
||||||
if (deletingCurrentCover) {
|
if (deletingCurrentCover) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -481,36 +481,63 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
|
|||||||
scrollController.dispose();
|
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(
|
return InkWell(
|
||||||
customBorder: const RoundedRectangleBorder(
|
customBorder: const RoundedRectangleBorder(
|
||||||
borderRadius: Corners.s6Border,
|
borderRadius: Corners.s6Border,
|
||||||
),
|
),
|
||||||
hoverColor: widget.pickerItemHoverColor,
|
hoverColor: hoverColor,
|
||||||
onTap: () {
|
onTap: () => onTap(option.colorHex),
|
||||||
widget.onSubmittedBackgroundColorHex(option.colorHex);
|
|
||||||
},
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 10.0),
|
padding: const EdgeInsets.only(right: 10.0),
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: isChecked ? 24 : 25,
|
dimension: 25,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: option.colorHex.toColor(),
|
color: option.colorHex.toColor(),
|
||||||
border: isChecked
|
|
||||||
? Border.all(
|
|
||||||
color: const Color(0xFFFFFFFF),
|
|
||||||
width: 2.0,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: isChecked
|
child: isChecked
|
||||||
? SizedBox.square(
|
? SizedBox.square(
|
||||||
dimension: 24,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
color: option.colorHex.toColor(),
|
color: option.colorHex.toColor(),
|
||||||
shape: BoxShape.circle,
|
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
part 'change_cover_popover_bloc.freezed.dart';
|
part 'cover_editor_bloc.freezed.dart';
|
||||||
|
|
||||||
class ChangeCoverPopoverBloc
|
class ChangeCoverPopoverBloc
|
||||||
extends Bloc<ChangeCoverPopoverEvent, ChangeCoverPopoverState> {
|
extends Bloc<ChangeCoverPopoverEvent, ChangeCoverPopoverState> {
|
||||||
@ -32,7 +32,7 @@ class ChangeCoverPopoverBloc
|
|||||||
deleteImage: (DeleteImage deleteImage) async {
|
deleteImage: (DeleteImage deleteImage) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
final currentlySelectedImage =
|
final currentlySelectedImage =
|
||||||
node.attributes[CoverBlockKeys.selection];
|
node.attributes[DocumentHeaderBlockKeys.coverDetails];
|
||||||
if (currentState is Loaded) {
|
if (currentState is Loaded) {
|
||||||
await _deleteImageInStorage(deleteImage.path);
|
await _deleteImageInStorage(deleteImage.path);
|
||||||
if (currentlySelectedImage == deleteImage.path) {
|
if (currentlySelectedImage == deleteImage.path) {
|
||||||
@ -48,7 +48,7 @@ class ChangeCoverPopoverBloc
|
|||||||
clearAllImages: (ClearAllImages clearAllImages) async {
|
clearAllImages: (ClearAllImages clearAllImages) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
final currentlySelectedImage =
|
final currentlySelectedImage =
|
||||||
node.attributes[CoverBlockKeys.selection];
|
node.attributes[DocumentHeaderBlockKeys.coverDetails];
|
||||||
|
|
||||||
if (currentState is Loaded) {
|
if (currentState is Loaded) {
|
||||||
for (final image in currentState.imageNames) {
|
for (final image in currentState.imageNames) {
|
||||||
@ -90,9 +90,9 @@ class ChangeCoverPopoverBloc
|
|||||||
Future<void> _removeCoverImageFromNode() async {
|
Future<void> _removeCoverImageFromNode() async {
|
||||||
final transaction = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
transaction.updateNode(node, {
|
transaction.updateNode(node, {
|
||||||
CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(),
|
DocumentHeaderBlockKeys.coverType: CoverType.none.toString(),
|
||||||
CoverBlockKeys.iconSelection:
|
DocumentHeaderBlockKeys.icon:
|
||||||
node.attributes[CoverBlockKeys.iconSelection]
|
node.attributes[DocumentHeaderBlockKeys.icon]
|
||||||
});
|
});
|
||||||
return editorState.apply(transaction);
|
return editorState.apply(transaction);
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
@ -13,9 +13,10 @@ import 'package:dartz/dartz.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:path/path.dart' as p;
|
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
|
class CoverImagePickerBloc
|
||||||
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
|
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
|
@ -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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,12 +5,10 @@ class EmojiIconWidget extends StatefulWidget {
|
|||||||
const EmojiIconWidget({
|
const EmojiIconWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.emoji,
|
required this.emoji,
|
||||||
this.size = 80,
|
|
||||||
this.emojiSize = 60,
|
this.emojiSize = 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String emoji;
|
final String emoji;
|
||||||
final double size;
|
|
||||||
final double emojiSize;
|
final double emojiSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -25,12 +23,11 @@ class _EmojiIconWidgetState extends State<EmojiIconWidget> {
|
|||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) => setHidden(false),
|
onEnter: (_) => setHidden(false),
|
||||||
onExit: (_) => setHidden(true),
|
onExit: (_) => setHidden(true),
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: widget.size,
|
|
||||||
width: widget.size,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: !hover
|
color: !hover
|
||||||
? Theme.of(context).colorScheme.inverseSurface
|
? Theme.of(context).colorScheme.inverseSurface.withOpacity(0.5)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
@ -30,38 +30,35 @@ class EmojiPopover extends StatefulWidget {
|
|||||||
class _EmojiPopoverState extends State<EmojiPopover> {
|
class _EmojiPopoverState extends State<EmojiPopover> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Column(
|
||||||
padding: const EdgeInsets.all(15),
|
children: [
|
||||||
child: Column(
|
if (widget.showRemoveButton)
|
||||||
children: [
|
Padding(
|
||||||
if (widget.showRemoveButton)
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
Padding(
|
child: Align(
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
alignment: Alignment.centerRight,
|
||||||
child: Align(
|
child: DeleteButton(onTap: widget.removeIcon),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyButton(
|
return SizedBox(
|
||||||
onTap: onTap,
|
height: 28,
|
||||||
useIntrinsicWidth: true,
|
child: FlowyButton(
|
||||||
text: FlowyText(
|
onTap: onTap,
|
||||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
useIntrinsicWidth: true,
|
||||||
|
text: FlowyText(
|
||||||
|
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||||
|
),
|
||||||
|
leftIcon: const FlowySvg(name: 'editor/delete'),
|
||||||
),
|
),
|
||||||
leftIcon: const FlowySvg(name: 'editor/delete'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
export 'callout/callout_block_component.dart';
|
export 'callout/callout_block_component.dart';
|
||||||
export 'code_block/code_block_component.dart';
|
export 'code_block/code_block_component.dart';
|
||||||
export 'code_block/code_block_shortcut_event.dart';
|
export 'code_block/code_block_shortcut_event.dart';
|
||||||
export 'cover/change_cover_popover_bloc.dart';
|
export 'header/cover_editor_bloc.dart';
|
||||||
export 'cover/cover_node_widget.dart';
|
export 'header/document_header_node_widget.dart';
|
||||||
export 'cover/cover_image_picker.dart';
|
export 'header/custom_cover_picker.dart';
|
||||||
export 'emoji_picker/emoji_menu_item.dart';
|
export 'emoji_picker/emoji_menu_item.dart';
|
||||||
export 'extensions/flowy_tint_extension.dart';
|
export 'extensions/flowy_tint_extension.dart';
|
||||||
export 'database/inline_database_menu_item.dart';
|
export 'database/inline_database_menu_item.dart';
|
||||||
|
@ -81,7 +81,7 @@ class FlowyIconButton extends StatelessWidget {
|
|||||||
hoverColor: hoverColor,
|
hoverColor: hoverColor,
|
||||||
foregroundColorOnHover:
|
foregroundColorOnHover:
|
||||||
iconColorOnHover ?? Theme.of(context).iconTheme.color,
|
iconColorOnHover ?? Theme.of(context).iconTheme.color,
|
||||||
backgroundColor: fillColor ?? Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: iconPadding,
|
padding: iconPadding,
|
||||||
|
Reference in New Issue
Block a user