Implement cover plugin 1868 (#1897)

* implement_cover_plugin_#1868

* code cleanup

* fix: CI issue fix

* fix: cover plugin implementation finalized

* fix: localization fixes

* fix: added add cover button

* chore: optimize the cover plugin code

* feat: auto hide the add button and cover buttons when leaving

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Muhammad Rizwan 2023-03-02 16:34:22 +05:00 committed by GitHub
parent fd41459a30
commit f1316acfcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 804 additions and 64 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@ -353,7 +353,15 @@
"smartEditFixSpelling": "Fix spelling",
"smartEditSummarize": "Summarize",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key"
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"cover": {
"changeCover": "Change Cover",
"colors": "Colors",
"images": "Images",
"abstract": "Abstract",
"addCover": "Add Cover",
"addLocalImage": "Add local image"
}
}
},
"board": {
@ -371,4 +379,4 @@
"nextMonth": "Next Month"
}
}
}
}

View File

@ -1,5 +1,6 @@
import 'package:appflowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/sm
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -126,9 +128,11 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final autoFocusParamters = _autoFocusParamters();
final editor = AppFlowyEditor(
editorState: editorState,
autoFocus: editorState.document.isEmpty,
autoFocus: autoFocusParamters.value1,
focusedSelection: autoFocusParamters.value2,
customBuilders: {
// Divider
kDividerType: DividerWidgetBuilder(),
@ -144,6 +148,8 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
kCalloutType: CalloutNodeWidgetBuilder(),
// Auto Generator,
kAutoCompletionInputType: AutoCompletionInputBuilder(),
// Cover
kCoverType: CoverNodeWidgetBuilder(),
// Smart Edit,
kSmartEditType: SmartEditInputBuilder(),
},
@ -174,7 +180,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
// enable open ai features if needed.
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
autoGeneratorMenuItem,
]
],
],
toolbarItems: [
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
@ -229,4 +235,18 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
await editorState.apply(transaction, withUpdateCursor: false);
}
}
dartz.Tuple2<bool, Selection?> _autoFocusParamters() {
if (editorState.document.isEmpty) {
return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
}
final texts = editorState.document.root.children.whereType<TextNode>();
if (texts.every((element) => element.toPlainText().isEmpty)) {
return dartz.Tuple2(
true,
Selection.single(path: texts.first.path, startOffset: 0),
);
}
return const dartz.Tuple2(false, null);
}
}

View File

@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
? EditorStyle.dark
: EditorStyle.light;
editorStyle = editorStyle.copyWith(
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 28),
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 0),
textStyle: editorStyle.textStyle?.copyWith(
fontFamily: 'poppins',
fontSize: documentStyle.fontSize,

View File

@ -0,0 +1,376 @@
import 'dart:io';
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.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';
const String kLocalImagesKey = 'local_images';
List<String> get builtInAssetImages => [
"assets/images/app_flowy_abstract_cover_1.jpg",
"assets/images/app_flowy_abstract_cover_2.jpg"
];
class ChangeCoverPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final Function(
CoverSelectionType selectionType,
String selection,
) onCoverChanged;
const ChangeCoverPopover({
super.key,
required this.editorState,
required this.onCoverChanged,
required this.node,
});
@override
State<ChangeCoverPopover> createState() => _ChangeCoverPopoverState();
}
class ColorOption {
final String colorHex;
final String name;
const ColorOption({
required this.colorHex,
required this.name,
});
}
class CoverColorPicker extends StatefulWidget {
final String? selectedBackgroundColorHex;
final Color pickerBackgroundColor;
final Color pickerItemHoverColor;
final void Function(String color) onSubmittedbackgroundColorHex;
final List<ColorOption> backgroundColorOptions;
const CoverColorPicker({
super.key,
this.selectedBackgroundColorHex,
required this.pickerBackgroundColor,
required this.backgroundColorOptions,
required this.pickerItemHoverColor,
required this.onSubmittedbackgroundColorHex,
});
@override
State<CoverColorPicker> createState() => _CoverColorPickerState();
}
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
late Future<List<String>>? fileImages;
@override
void initState() {
super.initState();
fileImages = _getPreviouslyPickedImagePaths();
}
@override
Widget build(BuildContext context) {
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(),
],
),
),
);
}
Widget _buildAbstractImagePicker() {
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1 / 0.65,
crossAxisSpacing: 7,
mainAxisSpacing: 7),
itemCount: builtInAssetImages.length,
itemBuilder: (BuildContext ctx, index) {
return InkWell(
onTap: () {
widget.onCoverChanged(
CoverSelectionType.asset,
builtInAssetImages[index],
);
},
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(builtInAssetImages[index]),
fit: BoxFit.cover,
),
borderRadius: Corners.s8Border,
),
),
);
},
);
}
Widget _buildColorPickerList() {
return CoverColorPicker(
pickerBackgroundColor:
widget.editorState.editorStyle.selectionMenuBackgroundColor ??
Colors.white,
pickerItemHoverColor:
widget.editorState.editorStyle.selectionMenuItemSelectedColor ??
Colors.blue.withOpacity(0.3),
selectedBackgroundColorHex:
widget.node.attributes[kCoverSelectionTypeAttribute] ==
CoverSelectionType.color.toString()
? widget.node.attributes[kCoverSelectionAttribute]
: "ffffff",
backgroundColorOptions:
_generateBackgroundColorOptions(widget.editorState),
onSubmittedbackgroundColorHex: (color) {
widget.onCoverChanged(CoverSelectionType.color, color);
setState(() {});
},
);
}
Widget _buildFileImagePicker() {
return FutureBuilder<List<String>>(
future: _getPreviouslyPickedImagePaths(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<String> images = snapshot.data!;
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1 / 0.65,
crossAxisSpacing: 7,
mainAxisSpacing: 7,
),
itemCount: images.length + 1,
itemBuilder: (BuildContext ctx, index) {
if (index == 0) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.15),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: Corners.s8Border,
),
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
width: 20,
onPressed: () {
_pickImages();
},
),
);
}
return InkWell(
onTap: () {
widget.onCoverChanged(
CoverSelectionType.file,
images[index - 1],
);
},
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: FileImage(File(images[index - 1])),
fit: BoxFit.cover,
),
borderRadius: Corners.s8Border,
),
),
);
},
);
} else {
return Container();
}
});
}
List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
return FlowyTint.values
.map((t) => ColorOption(
colorHex: t.color(context).toHex(),
name: t.tintName(AppFlowyEditorLocalizations.current),
))
.toList();
}
Future<List<String>> _getPreviouslyPickedImagePaths() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final imageNames = prefs.getStringList(kLocalImagesKey) ?? [];
final removeNames = [];
for (final name in imageNames) {
if (!File(name).existsSync()) {
removeNames.add(name);
}
}
imageNames.removeWhere((element) => removeNames.contains(element));
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('/').last}',
);
imageNames.add(newPath.path);
}
}
await prefs.setStringList(kLocalImagesKey, imageNames);
setState(() {});
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory('$directory/covers')
.create(recursive: true)
.then((value) => value.path);
}
}
class _CoverColorPickerState extends State<CoverColorPicker> {
final scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Container(
height: 30,
alignment: Alignment.center,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
}, platform: TargetPlatform.windows),
child: ListView.builder(
controller: scrollController,
shrinkWrap: true,
itemCount: widget.backgroundColorOptions.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return _buildColorItems(
widget.backgroundColorOptions,
widget.selectedBackgroundColorHex,
);
},
),
),
);
}
@override
void dispose() {
super.dispose();
scrollController.dispose();
}
Widget _buildColorItem(ColorOption option, bool isChecked) {
return InkWell(
customBorder: const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
hoverColor: widget.pickerItemHoverColor,
onTap: () {
widget.onSubmittedbackgroundColorHex(option.colorHex);
},
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: SizedBox.square(
dimension: 25,
child: Container(
decoration: BoxDecoration(
color: isChecked
? Colors.transparent
: Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
border: isChecked
? Border.all(
color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFF))
: null,
shape: BoxShape.circle,
),
child: isChecked
? SizedBox.square(
dimension: 25,
child: Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color:
Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
shape: BoxShape.circle,
),
),
)
: null,
),
),
),
);
}
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(),
);
}
}
extension on Color {
String toHex() {
return '0x${value.toRadixString(16)}';
}
}

View File

@ -0,0 +1,302 @@
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_editor/appflowy_editor.dart';
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/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
const String kCoverType = 'cover';
const String kCoverSelectionTypeAttribute = 'cover_selection_type';
const String kCoverSelectionAttribute = 'cover_selection';
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[kCoverSelectionTypeAttribute],
);
@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);
},
);
}
}
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute: type.toString(),
kCoverSelectionAttribute: cover,
});
return widget.editorState.apply(transaction);
}
}
class _AddCoverButton extends StatefulWidget {
const _AddCoverButton({
required this.onTap,
});
final VoidCallback onTap;
@override
State<_AddCoverButton> createState() => _AddCoverButtonState();
}
class _AddCoverButtonState extends State<_AddCoverButton> {
bool isHidden = true;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setHidden(false);
},
onExit: (event) {
setHidden(true);
},
child: Container(
height: 50.0,
width: double.infinity,
padding: const EdgeInsets.only(top: 20, bottom: 5),
// color: Colors.red,
child: isHidden
? const SizedBox()
: 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(),
),
)
// Add Icon Button.
// ...
],
),
),
);
}
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[kCoverSelectionTypeAttribute],
);
Color get color =>
Color(int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
0xFFFFFFFF);
bool isOverlayButtonsHidden = true;
@override
Widget build(BuildContext context) {
return Stack(
children: [
_buildCoverImage(context),
_buildCoverOverlayButtons(context),
],
);
}
Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned(
bottom: 22,
right: 12,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AppFlowyPopover(
offset: const Offset(-125, 10),
controller: popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(380, 450)),
margin: EdgeInsets.zero,
child: RoundedTextButton(
onPressed: () {
popoverController.show();
},
hoverColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.onSurface,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
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),
FlowyIconButton(
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
hoverColor: Theme.of(context).colorScheme.surface,
iconPadding: const EdgeInsets.all(5),
width: 28,
icon: svgWidget(
'editor/delete',
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
widget.onCoverChanged(CoverSelectionType.initial, null);
},
),
],
),
);
}
Widget _buildCoverImage(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
const height = 200.0;
final Widget coverImage;
switch (selectionType) {
case CoverSelectionType.file:
coverImage = Image.file(
File(widget.node.attributes[kCoverSelectionAttribute]),
fit: BoxFit.cover,
);
break;
case CoverSelectionType.asset:
coverImage = Image.asset(
widget.node.attributes[kCoverSelectionAttribute],
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(); // just an empty sizebox
break;
}
return UnconstrainedBox(
child: Container(
padding: const EdgeInsets.only(bottom: 10),
height: height,
width: screenSize.width,
child: coverImage,
),
);
}
void setOverlayButtonsHidden(bool value) {
if (isOverlayButtonsHidden == value) return;
setState(() {
isOverlayButtonsHidden = value;
});
}
}

View File

@ -1,9 +1,10 @@
import 'package:appflowy/plugins/document/document.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
import 'package:appflowy_editor/appflowy_editor.dart' show Document, Node;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -60,7 +61,12 @@ class AddButton extends StatelessWidget {
},
onSelected: (action, controller) {
if (action is AddButtonActionWrapper) {
onSelected(action.pluginBuilder, null);
Document? document;
if (action.pluginType == PluginType.editor) {
// initialize the document if needed.
document = buildInitialDocument();
}
onSelected(action.pluginBuilder, document);
}
if (action is ImportActionWrapper) {
showImportPanel(context, (document) {
@ -74,6 +80,12 @@ class AddButton extends StatelessWidget {
},
);
}
Document buildInitialDocument() {
final document = Document.empty();
document.insert([0], [Node(type: kCoverType)]);
return document;
}
}
class AddButtonActionWrapper extends ActionCell {
@ -87,6 +99,8 @@ class AddButtonActionWrapper extends ActionCell {
@override
String get name => pluginBuilder.menuName;
PluginType get pluginType => pluginBuilder.pluginType;
}
class ImportActionWrapper extends ActionCell {

View File

@ -1,4 +1,3 @@
#include "include/appflowy_backend/appflowy_flutter_backend_plugin.h"
// This must be included before many other Windows headers.
#include <windows.h>
@ -13,70 +12,85 @@
#include <map>
#include <memory>
#include <sstream>
#include "include/appflowy_backend/app_flowy_backend_plugin.h"
namespace {
namespace
{
class AppFlowyBackendPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
class AppFlowyBackendPlugin : public flutter::Plugin
{
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
AppFlowyBackendPlugin();
AppFlowyBackendPlugin();
virtual ~AppFlowyBackendPlugin();
virtual ~AppFlowyBackendPlugin();
private:
// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
};
private:
// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
};
// static
void AppFlowyBackendPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows *registrar) {
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "appflowy_backend",
&flutter::StandardMethodCodec::GetInstance());
// static
void AppFlowyBackendPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows *registrar)
{
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "appflowy_backend",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<AppFlowyBackendPlugin>();
auto plugin = std::make_unique<AppFlowyBackendPlugin>();
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result)
{
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
AppFlowyBackendPlugin::AppFlowyBackendPlugin() {}
AppFlowyBackendPlugin::~AppFlowyBackendPlugin() {}
void AppFlowyBackendPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("getPlatformVersion") == 0) {
std::ostringstream version_stream;
version_stream << "Windows ";
if (IsWindows10OrGreater()) {
version_stream << "10+";
} else if (IsWindows8OrGreater()) {
version_stream << "8";
} else if (IsWindows7OrGreater()) {
version_stream << "7";
}
result->Success(flutter::EncodableValue(version_stream.str()));
} else {
result->NotImplemented();
registrar->AddPlugin(std::move(plugin));
}
}
} // namespace
AppFlowyBackendPlugin::AppFlowyBackendPlugin() {}
AppFlowyBackendPlugin::~AppFlowyBackendPlugin() {}
void AppFlowyBackendPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
if (method_call.method_name().compare("getPlatformVersion") == 0)
{
std::ostringstream version_stream;
version_stream << "Windows ";
if (IsWindows10OrGreater())
{
version_stream << "10+";
}
else if (IsWindows8OrGreater())
{
version_stream << "8";
}
else if (IsWindows7OrGreater())
{
version_stream << "7";
}
result->Success(flutter::EncodableValue(version_stream.str()));
}
else
{
result->NotImplemented();
}
}
} // namespace
void AppFlowyBackendPluginRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
FlutterDesktopPluginRegistrarRef registrar)
{
AppFlowyBackendPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}
}

View File

@ -5,8 +5,9 @@
#include "appflowy_flutter_backend_plugin.h"
void AppFlowyBackendPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
FlutterDesktopPluginRegistrarRef registrar)
{
appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}

View File

@ -32,6 +32,7 @@ class AppFlowyEditor extends StatefulWidget {
this.toolbarItems = const [],
this.editable = true,
this.autoFocus = false,
this.focusedSelection,
this.customActionMenuBuilder,
ThemeData? themeData,
}) : super(key: key) {
@ -60,6 +61,7 @@ class AppFlowyEditor extends StatefulWidget {
/// Set the value to true to focus the editor on the start of the document.
final bool autoFocus;
final Selection? focusedSelection;
final Positioned Function(BuildContext context, List<ActionMenuItem> items)?
customActionMenuBuilder;
@ -89,7 +91,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.editable && widget.autoFocus) {
editorState.service.selectionService.updateSelection(
Selection.single(path: [0], startOffset: 0),
widget.focusedSelection ??
Selection.single(path: [0], startOffset: 0),
);
}
});

View File

@ -12,3 +12,5 @@ export 'src/divider/divider_shortcut_event.dart';
export 'src/emoji_picker/emoji_menu_item.dart';
// Math Equation
export 'src/math_ equation/math_equation_node_widget.dart';
export 'src/extensions/theme_extension.dart';