Improvement for cover plugin #1928 (#1981)

* feat: added emoji and network image support

* fix: code cleanup and improvements

* fix: blank preview on invalid image save

* fix: flutter analyzer warnings

* fix: code refactor and bug fixes

* chore: removed unused imports

* chore: formate code

---------

Co-authored-by: ahmeduzair890 <ahmeduzair12123@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Muhammad Rizwan
2023-03-15 10:22:48 +05:00
committed by GitHub
parent 7be7c2a7a0
commit 4c0168d7fa
10 changed files with 891 additions and 99 deletions

View File

@ -2,21 +2,17 @@ import 'dart:io';
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' show FileType;
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as path;
const String kLocalImagesKey = 'local_images';
@ -76,6 +72,7 @@ class CoverColorPicker extends StatefulWidget {
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
late Future<List<String>>? fileImages;
bool isAddingImage = false;
@override
void initState() {
@ -88,26 +85,40 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
return Padding(
padding: const EdgeInsets.all(15),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()),
const SizedBox(height: 10),
_buildColorPickerList(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()),
const SizedBox(height: 10),
_buildFileImagePicker(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
const SizedBox(height: 10),
_buildAbstractImagePicker(),
],
),
child: isAddingImage
? CoverImagePicker(
onBackPressed: () => setState(() {
isAddingImage = false;
}),
onFileSubmit: (List<String> path) {
setState(() {
isAddingImage = false;
});
})
: _buildCoverSelection(),
),
);
}
Widget _buildCoverSelection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()),
const SizedBox(height: 10),
_buildColorPickerList(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()),
const SizedBox(height: 10),
_buildFileImagePicker(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
const SizedBox(height: 10),
_buildAbstractImagePicker(),
],
);
}
Widget _buildAbstractImagePicker() {
return GridView.builder(
shrinkWrap: true,
@ -197,7 +208,9 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
),
width: 20,
onPressed: () {
_pickImages();
setState(() {
isAddingImage = true;
});
},
),
);
@ -249,36 +262,6 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
prefs.setStringList(kLocalImagesKey, imageNames);
return imageNames;
}
Future<void> _pickImages() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> imageNames = prefs.getStringList(kLocalImagesKey) ?? [];
FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'],
);
if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path;
if (path != null) {
final directory = await _coverPath();
final newPath = await File(path).copy(
'$directory/${path.split(path).last}}',
);
imageNames.add(newPath.path);
}
}
await prefs.setStringList(kLocalImagesKey, imageNames);
setState(() {});
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory(path.join(directory, 'covers'))
.create(recursive: true)
.then((value) => value.path);
}
}
class _CoverColorPickerState extends State<CoverColorPicker> {

View File

@ -0,0 +1,254 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_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';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class CoverImagePicker extends StatefulWidget {
final VoidCallback onBackPressed;
final Function(List<String> paths) onFileSubmit;
const CoverImagePicker(
{super.key, required this.onBackPressed, required this.onFileSubmit});
@override
State<CoverImagePicker> createState() => _CoverImagePickerState();
}
class _CoverImagePickerState extends State<CoverImagePicker> {
TextEditingController urlController = TextEditingController();
bool get buttonDisabled => urlController.text.isEmpty;
@override
void initState() {
super.initState();
urlController.addListener(() {
setState(() {});
});
}
_buildFilePickerWidget(BuildContext ctx) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
svgWidget(
"editor/add",
size: const Size(20, 20),
),
const SizedBox(
width: 3,
),
FlowyText(
LocaleKeys.document_plugins_cover_pasteImageUrl.tr(),
),
],
),
const SizedBox(
height: 10,
),
FlowyText(
LocaleKeys.document_plugins_cover_or.tr(),
color: Colors.grey,
),
const SizedBox(
height: 10,
),
FlowyButton(
onTap: () {
ctx.read<CoverImagePickerBloc>().add(const PickFileImage());
},
useIntrinsicWidth: true,
leftIcon: svgWidget(
"file_icon",
size: const Size(25, 25),
),
text: FlowyText(
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
),
),
],
);
}
_buildImageDeleteButton(BuildContext ctx) {
return Positioned(
right: 10,
top: 10,
child: InkWell(
onTap: () {
ctx.read<CoverImagePickerBloc>().add(const DeleteImage());
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.onPrimary),
child: svgWidget(
"editor/close",
size: const Size(20, 20),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CoverImagePickerBloc()
..add(const CoverImagePickerEvent.initialEvent()),
child: BlocListener<CoverImagePickerBloc, CoverImagePickerState>(
listener: (context, state) {
if (state is NetworkImagePicked) {
state.successOrFail.isRight()
? showSnapBar(context,
LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
: null;
}
if (state is Done) {
state.successOrFail.fold(
(l) => widget.onFileSubmit(l),
(r) => showSnapBar(
context,
LocaleKeys.document_plugins_cover_failedToAddImageToGallery
.tr()));
}
},
child: BlocBuilder<CoverImagePickerBloc, CoverImagePickerState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
state is Loading
? const SizedBox(
height: 180,
child: Center(
child: CircularProgressIndicator(),
),
)
: Stack(
children: [
Container(
height: 180,
alignment: Alignment.center,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondary,
borderRadius: Corners.s6Border,
image: state is Initial
? null
: state is NetworkImagePicked
? state.successOrFail.fold(
(path) => DecorationImage(
image: NetworkImage(path),
fit: BoxFit.cover),
(r) => null)
: state is FileImagePicked
? DecorationImage(
image: FileImage(
File(state.path)),
fit: BoxFit.cover)
: null),
child: (state is Initial)
? _buildFilePickerWidget(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => null,
(r) => _buildFilePickerWidget(
context,
),
)
: null),
(state is FileImagePicked)
? _buildImageDeleteButton(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => _buildImageDeleteButton(context),
(r) => Container())
: Container()
],
),
const SizedBox(
height: 10,
),
Row(
children: [
Expanded(
flex: 4,
child: FlowyTextField(
controller: urlController,
hintText: LocaleKeys
.document_plugins_cover_enterImageUrl
.tr(),
),
),
const SizedBox(
width: 5,
),
Expanded(
flex: 1,
child: RoundedTextButton(
onPressed: () {
urlController.text.isNotEmpty
? context
.read<CoverImagePickerBloc>()
.add(UrlSubmit(urlController.text))
: null;
},
hoverColor: Colors.transparent,
fillColor: buttonDisabled
? Colors.grey
: Theme.of(context).colorScheme.primary,
height: 36,
title: LocaleKeys.document_plugins_cover_add.tr(),
borderRadius: Corners.s8Border,
),
)
],
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyTextButton(
LocaleKeys.document_plugins_cover_back.tr(),
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
onPressed: () => widget.onBackPressed(),
),
FlowyTextButton(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
onPressed: () async {
context
.read<CoverImagePickerBloc>()
.add(SaveToGallery(state));
},
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
fontColor: Theme.of(context).colorScheme.primary,
),
],
)
],
);
},
),
),
);
}
}

View File

@ -0,0 +1,196 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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 path;
import 'change_cover_popover.dart';
part 'cover_image_picker_bloc.freezed.dart';
class CoverImagePickerBloc
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
on<CoverImagePickerEvent>(
(event, emit) async {
await event.map(
initialEvent: (InitialEvent initialEvent) {
emit(const CoverImagePickerState.initial());
},
urlSubmit: (UrlSubmit urlSubmit) async {
emit(const CoverImagePickerState.loading());
final validateImage = await _validateUrl(urlSubmit.path);
if (validateImage) {
emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
} else {
emit(
CoverImagePickerState.networkImage(
right(
FlowyError(
msg: LocaleKeys.document_plugins_cover_couldNotFetchImage
.tr(),
),
),
),
);
}
},
pickFileImage: (PickFileImage pickFileImage) async {
final imagePickerResults = await _pickImages();
if (imagePickerResults != null) {
emit(CoverImagePickerState.fileImage(imagePickerResults));
} else {
emit(const CoverImagePickerState.initial());
}
},
deleteImage: (DeleteImage deleteImage) {
emit(const CoverImagePickerState.initial());
},
saveToGallery: (SaveToGallery saveToGallery) async {
emit(const CoverImagePickerState.loading());
final saveImage = await _saveToGallery(saveToGallery.previousState);
if (saveImage != null) {
emit(CoverImagePickerState.done(left(saveImage)));
} else {
emit(
CoverImagePickerState.done(
right(
FlowyError(
msg: LocaleKeys.document_plugins_cover_imageSavingFailed
.tr()),
),
),
);
emit(const CoverImagePickerState.initial());
}
},
);
},
);
}
_saveToGallery(CoverImagePickerState state) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> imagePaths = prefs.getStringList(kLocalImagesKey) ?? [];
final directory = await _coverPath();
if (state is FileImagePicked) {
try {
final path = state.path;
final newPath = '$directory/${path.split("\\").last}';
final newFile = await File(path).copy(newPath);
imagePaths.add(newFile.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} catch (e) {
return null;
}
} else if (state is NetworkImagePicked) {
try {
String? url = state.successOrFail.fold((path) => path, (r) => null);
if (url != null) {
final response = await http.get(Uri.parse(url));
final newPath =
"$directory/IMG_$_timeStampString.${_getExtention(url)}";
final imageFile = File(newPath);
await imageFile.create();
await imageFile.writeAsBytes(response.bodyBytes);
imagePaths.add(imageFile.absolute.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} else {
return null;
}
} catch (e) {
return null;
}
}
}
_pickImages() async {
FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: fp.FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'],
);
if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path;
if (path != null) {
return path;
} else {
return null;
}
}
return null;
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory(path.join(directory, 'covers'))
.create(recursive: true)
.then((value) => value.path);
}
String get _timeStampString =>
DateTime.now().millisecondsSinceEpoch.toString();
String? _getExtention(String path) => path.contains(".jpg")
? "jpg"
: path.contains(".png")
? "png"
: path.contains(".jpeg")
? "jpeg"
: (path.contains("auto=format") && path.contains("unsplash"))
? "jpeg"
: null;
_validateUrl(String path) async {
if (_getExtention(path) != null) {
try {
final response = await http.get(Uri.parse(path));
if (response.statusCode == 200) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
} else {
return false;
}
}
}
@freezed
class CoverImagePickerEvent with _$CoverImagePickerEvent {
const factory CoverImagePickerEvent.urlSubmit(String path) = UrlSubmit;
const factory CoverImagePickerEvent.pickFileImage() = PickFileImage;
const factory CoverImagePickerEvent.deleteImage() = DeleteImage;
const factory CoverImagePickerEvent.saveToGallery(
CoverImagePickerState previousState) = SaveToGallery;
const factory CoverImagePickerEvent.initialEvent() = InitialEvent;
}
@freezed
class CoverImagePickerState with _$CoverImagePickerState {
const factory CoverImagePickerState.initial() = Initial;
const factory CoverImagePickerState.loading() = Loading;
const factory CoverImagePickerState.networkImage(
Either<String, FlowyError> successOrFail) = NetworkImagePicked;
const factory CoverImagePickerState.fileImage(String path) = FileImagePicked;
const factory CoverImagePickerState.done(
Either<List<String>, FlowyError> successOrFail) = Done;
}

View File

@ -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()
],
),
),
);

View File

@ -0,0 +1,92 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class EmojiPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final void Function(Emoji emoji) onEmojiChanged;
final VoidCallback removeIcon;
final bool showRemoveButton;
const EmojiPopover({
super.key,
required this.editorState,
required this.node,
required this.onEmojiChanged,
required this.removeIcon,
required this.showRemoveButton,
});
@override
State<EmojiPopover> createState() => _EmojiPopoverState();
}
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(15),
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
customWidget: (Config config, EmojiViewState state) {
return Stack(
alignment: Alignment.topRight,
children: [
Container(
padding: EdgeInsets.only(top: widget.showRemoveButton ? 25 : 0),
child: DefaultEmojiPickerView(config, state),
),
_buildDeleteButtonIfNeed(),
],
);
},
config: const Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Colors.grey,
iconColorSelected: Color(0xff333333),
indicatorColor: Color(0xff333333),
progressIndicatorColor: Color(0xff333333),
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
);
}
Widget _buildDeleteButtonIfNeed() {
if (!widget.showRemoveButton) {
return const SizedBox();
}
return FlowyButton(
onTap: () => widget.removeIcon(),
useIntrinsicWidth: true,
hoverColor: Theme.of(context).colorScheme.onPrimary,
text: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
svgWidget("editor/delete"),
const SizedBox(
width: 5,
),
FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
color: Colors.grey,
),
],
),
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmojiIconWidget extends StatefulWidget {
final String? emoji;
final void Function() onEmojiTapped;
const EmojiIconWidget({
super.key,
required this.emoji,
required this.onEmojiTapped,
});
@override
State<EmojiIconWidget> createState() => _EmojiIconWidgetState();
}
class _EmojiIconWidgetState extends State<EmojiIconWidget> {
bool hover = true;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setHidden(false);
},
onExit: (event) {
setHidden(true);
},
child: Container(
height: 130,
width: 130,
margin: const EdgeInsets.only(top: 18),
decoration: BoxDecoration(
color: !hover
? Theme.of(context).colorScheme.secondary
: Colors.transparent,
borderRadius: Corners.s8Border,
),
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
FlowyText(
widget.emoji.toString(),
fontSize: 80,
),
],
),
),
);
}
void setHidden(bool value) {
if (hover == value) return;
setState(() {
hover = value;
});
}
}