mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
* 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:
@ -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> {
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user