|
@@ -2,6 +2,9 @@ import 'dart:io';
|
|
|
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
|
import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
|
|
|
+import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
|
|
|
+import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
|
|
|
+import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
|
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
@@ -14,10 +17,10 @@ import 'package:flutter/material.dart';
|
|
|
const String kCoverType = 'cover';
|
|
|
const String kCoverSelectionTypeAttribute = 'cover_selection_type';
|
|
|
const String kCoverSelectionAttribute = 'cover_selection';
|
|
|
+const String kIconSelectionAttribute = 'selected_icon';
|
|
|
|
|
|
enum CoverSelectionType {
|
|
|
initial,
|
|
|
-
|
|
|
color,
|
|
|
file,
|
|
|
asset;
|
|
@@ -68,23 +71,16 @@ class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
|
|
|
widget.node.attributes[kCoverSelectionTypeAttribute],
|
|
|
);
|
|
|
|
|
|
+ PopoverController iconPopoverController = PopoverController();
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
- if (selectionType == CoverSelectionType.initial) {
|
|
|
- return _AddCoverButton(
|
|
|
- onTap: () {
|
|
|
- _insertCover(CoverSelectionType.asset, builtInAssetImages.first);
|
|
|
- },
|
|
|
- );
|
|
|
- } else {
|
|
|
- return _CoverImage(
|
|
|
- editorState: widget.editorState,
|
|
|
- node: widget.node,
|
|
|
- onCoverChanged: (type, value) {
|
|
|
- _insertCover(type, value);
|
|
|
- },
|
|
|
- );
|
|
|
- }
|
|
|
+ return _CoverImage(
|
|
|
+ editorState: widget.editorState,
|
|
|
+ node: widget.node,
|
|
|
+ onCoverChanged: (type, value) {
|
|
|
+ _insertCover(type, value);
|
|
|
+ },
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
|
|
@@ -92,14 +88,26 @@ class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
|
|
|
transaction.updateNode(widget.node, {
|
|
|
kCoverSelectionTypeAttribute: type.toString(),
|
|
|
kCoverSelectionAttribute: cover,
|
|
|
+ kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
|
|
|
});
|
|
|
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;
|
|
@@ -108,8 +116,16 @@ class _AddCoverButton extends StatefulWidget {
|
|
|
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) {
|
|
@@ -118,40 +134,118 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
|
|
|
setHidden(false);
|
|
|
},
|
|
|
onExit: (event) {
|
|
|
- setHidden(true);
|
|
|
+ setHidden(isPopoverOpen ? false : true);
|
|
|
},
|
|
|
+ opaque: false,
|
|
|
child: Container(
|
|
|
- height: 50.0,
|
|
|
+ height: widget.hasIcon ? 180 : 50.0,
|
|
|
+ alignment: Alignment.bottomLeft,
|
|
|
width: double.infinity,
|
|
|
padding: const EdgeInsets.only(top: 20, bottom: 5),
|
|
|
- // color: Colors.red,
|
|
|
child: isHidden
|
|
|
- ? const SizedBox()
|
|
|
+ ? Container()
|
|
|
: Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
children: [
|
|
|
// Add Cover Button.
|
|
|
- FlowyButton(
|
|
|
- leftIconSize: const Size.square(18),
|
|
|
- onTap: widget.onTap,
|
|
|
- useIntrinsicWidth: true,
|
|
|
- leftIcon: svgWidget(
|
|
|
- 'editor/image',
|
|
|
- color: Theme.of(context).colorScheme.onSurface,
|
|
|
- ),
|
|
|
- text: FlowyText.regular(
|
|
|
- LocaleKeys.document_plugins_cover_addCover.tr(),
|
|
|
- ),
|
|
|
- )
|
|
|
+ widget.selectionType != CoverSelectionType.initial
|
|
|
+ ? Container()
|
|
|
+ : FlowyButton(
|
|
|
+ key: UniqueKey(),
|
|
|
+ leftIconSize: const Size.square(18),
|
|
|
+ onTap: widget.onTap,
|
|
|
+ useIntrinsicWidth: true,
|
|
|
+ leftIcon: svgWidget(
|
|
|
+ 'editor/image',
|
|
|
+ color: Theme.of(context).colorScheme.onSurface,
|
|
|
+ ),
|
|
|
+ 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: Icon(
|
|
|
+ Icons.emoji_emotions_outlined,
|
|
|
+ color: Theme.of(context).colorScheme.onSurface,
|
|
|
+ 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: Icon(Icons.emoji_emotions_outlined,
|
|
|
+ color: Theme.of(context).colorScheme.onSurface,
|
|
|
+ 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, {
|
|
|
+ kCoverSelectionTypeAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionTypeAttribute],
|
|
|
+ kCoverSelectionAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionAttribute],
|
|
|
+ kIconSelectionAttribute: emoji.emoji,
|
|
|
+ });
|
|
|
+ return widget.editorState.apply(transaction);
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _removeIcon() async {
|
|
|
+ final transaction = widget.editorState.transaction;
|
|
|
+ transaction.updateNode(widget.node, {
|
|
|
+ kIconSelectionAttribute: "",
|
|
|
+ kCoverSelectionTypeAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionTypeAttribute],
|
|
|
+ kCoverSelectionAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionAttribute],
|
|
|
+ });
|
|
|
+ return widget.editorState.apply(transaction);
|
|
|
+ }
|
|
|
+
|
|
|
void setHidden(bool value) {
|
|
|
if (isHidden == value) return;
|
|
|
setState(() {
|
|
@@ -173,7 +267,6 @@ class _CoverImage extends StatefulWidget {
|
|
|
CoverSelectionType selectionType,
|
|
|
dynamic selection,
|
|
|
) onCoverChanged;
|
|
|
-
|
|
|
@override
|
|
|
State<_CoverImage> createState() => _CoverImageState();
|
|
|
}
|
|
@@ -187,23 +280,112 @@ class _CoverImageState extends State<_CoverImage> {
|
|
|
Color get color =>
|
|
|
Color(int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
|
|
|
0xFFFFFFFF);
|
|
|
-
|
|
|
+ bool get hasIcon => widget.node.attributes[kIconSelectionAttribute] == null
|
|
|
+ ? false
|
|
|
+ : widget.node.attributes[kIconSelectionAttribute].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: [
|
|
|
- _buildCoverImage(context, widget.editorState),
|
|
|
- _buildCoverOverlayButtons(context),
|
|
|
+ Container(
|
|
|
+ alignment: Alignment.topCenter,
|
|
|
+ height: !hasCover
|
|
|
+ ? 0
|
|
|
+ : hasIcon
|
|
|
+ ? 320
|
|
|
+ : 280,
|
|
|
+ child: _buildCoverImage(context, widget.editorState),
|
|
|
+ ),
|
|
|
+ hasIcon
|
|
|
+ ? Positioned(
|
|
|
+ bottom: !hasCover ? 30 : 10,
|
|
|
+ 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[kIconSelectionAttribute],
|
|
|
+ onEmojiTapped: () {
|
|
|
+ iconPopoverController.show();
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ 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, {
|
|
|
+ kCoverSelectionTypeAttribute: type.toString(),
|
|
|
+ kCoverSelectionAttribute: cover,
|
|
|
+ kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
|
|
|
+ });
|
|
|
+ return widget.editorState.apply(transaction);
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _insertIcon(Emoji emoji) async {
|
|
|
+ final transaction = widget.editorState.transaction;
|
|
|
+ transaction.updateNode(widget.node, {
|
|
|
+ kCoverSelectionTypeAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionTypeAttribute],
|
|
|
+ kCoverSelectionAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionAttribute],
|
|
|
+ kIconSelectionAttribute: emoji.emoji,
|
|
|
+ });
|
|
|
+ return widget.editorState.apply(transaction);
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _removeIcon() async {
|
|
|
+ final transaction = widget.editorState.transaction;
|
|
|
+ transaction.updateNode(widget.node, {
|
|
|
+ kIconSelectionAttribute: "",
|
|
|
+ kCoverSelectionTypeAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionTypeAttribute],
|
|
|
+ kCoverSelectionAttribute:
|
|
|
+ widget.node.attributes[kCoverSelectionAttribute],
|
|
|
+ });
|
|
|
+ return widget.editorState.apply(transaction);
|
|
|
+ }
|
|
|
+
|
|
|
Widget _buildCoverOverlayButtons(BuildContext context) {
|
|
|
return Positioned(
|
|
|
- bottom: 22,
|
|
|
- right: 12,
|
|
|
+ bottom: 20,
|
|
|
+ right: 260,
|
|
|
child: Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
@@ -253,7 +435,7 @@ class _CoverImageState extends State<_CoverImage> {
|
|
|
|
|
|
Widget _buildCoverImage(BuildContext context, EditorState editorState) {
|
|
|
final screenSize = MediaQuery.of(context).size;
|
|
|
- const height = 200.0;
|
|
|
+ const height = 250.0;
|
|
|
final Widget coverImage;
|
|
|
switch (selectionType) {
|
|
|
case CoverSelectionType.file:
|
|
@@ -278,7 +460,7 @@ class _CoverImageState extends State<_CoverImage> {
|
|
|
);
|
|
|
break;
|
|
|
case CoverSelectionType.initial:
|
|
|
- coverImage = const SizedBox(); // just an empty sizebox
|
|
|
+ coverImage = const SizedBox();
|
|
|
break;
|
|
|
}
|
|
|
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr
|
|
@@ -286,11 +468,16 @@ class _CoverImageState extends State<_CoverImage> {
|
|
|
height: height,
|
|
|
child: OverflowBox(
|
|
|
maxWidth: screenSize.width,
|
|
|
- child: Container(
|
|
|
- padding: const EdgeInsets.only(bottom: 10),
|
|
|
- height: double.infinity,
|
|
|
- width: double.infinity,
|
|
|
- child: coverImage,
|
|
|
+ child: Stack(
|
|
|
+ children: [
|
|
|
+ Container(
|
|
|
+ padding: const EdgeInsets.only(bottom: 10),
|
|
|
+ height: double.infinity,
|
|
|
+ width: double.infinity,
|
|
|
+ child: coverImage,
|
|
|
+ ),
|
|
|
+ hasCover ? _buildCoverOverlayButtons(context) : const SizedBox()
|
|
|
+ ],
|
|
|
),
|
|
|
),
|
|
|
);
|