mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 164 KiB |
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
@ -353,7 +353,15 @@
|
|||||||
"smartEditFixSpelling": "Fix spelling",
|
"smartEditFixSpelling": "Fix spelling",
|
||||||
"smartEditSummarize": "Summarize",
|
"smartEditSummarize": "Summarize",
|
||||||
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
|
"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": {
|
"board": {
|
||||||
|
@ -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_menu_item.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.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_menu_item.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.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';
|
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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.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:flowy_infra_ui/widget/error_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -126,9 +128,11 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final autoFocusParamters = _autoFocusParamters();
|
||||||
final editor = AppFlowyEditor(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
autoFocus: editorState.document.isEmpty,
|
autoFocus: autoFocusParamters.value1,
|
||||||
|
focusedSelection: autoFocusParamters.value2,
|
||||||
customBuilders: {
|
customBuilders: {
|
||||||
// Divider
|
// Divider
|
||||||
kDividerType: DividerWidgetBuilder(),
|
kDividerType: DividerWidgetBuilder(),
|
||||||
@ -144,6 +148,8 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
|||||||
kCalloutType: CalloutNodeWidgetBuilder(),
|
kCalloutType: CalloutNodeWidgetBuilder(),
|
||||||
// Auto Generator,
|
// Auto Generator,
|
||||||
kAutoCompletionInputType: AutoCompletionInputBuilder(),
|
kAutoCompletionInputType: AutoCompletionInputBuilder(),
|
||||||
|
// Cover
|
||||||
|
kCoverType: CoverNodeWidgetBuilder(),
|
||||||
// Smart Edit,
|
// Smart Edit,
|
||||||
kSmartEditType: SmartEditInputBuilder(),
|
kSmartEditType: SmartEditInputBuilder(),
|
||||||
},
|
},
|
||||||
@ -174,7 +180,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
|||||||
// enable open ai features if needed.
|
// enable open ai features if needed.
|
||||||
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
||||||
autoGeneratorMenuItem,
|
autoGeneratorMenuItem,
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
toolbarItems: [
|
toolbarItems: [
|
||||||
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
||||||
@ -229,4 +235,18 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
|||||||
await editorState.apply(transaction, withUpdateCursor: false);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
|
|||||||
? EditorStyle.dark
|
? EditorStyle.dark
|
||||||
: EditorStyle.light;
|
: EditorStyle.light;
|
||||||
editorStyle = editorStyle.copyWith(
|
editorStyle = editorStyle.copyWith(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 28),
|
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 0),
|
||||||
textStyle: editorStyle.textStyle?.copyWith(
|
textStyle: editorStyle.textStyle?.copyWith(
|
||||||
fontFamily: 'poppins',
|
fontFamily: 'poppins',
|
||||||
fontSize: documentStyle.fontSize,
|
fontSize: documentStyle.fontSize,
|
||||||
|
@ -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)}';
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:appflowy/plugins/document/document.dart';
|
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/plugin/plugin.dart';
|
||||||
import 'package:appflowy/startup/startup.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/home/menu/app/header/import/import_panel.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra/image.dart';
|
import 'package:flowy_infra/image.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
@ -60,7 +61,12 @@ class AddButton extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
onSelected: (action, controller) {
|
onSelected: (action, controller) {
|
||||||
if (action is AddButtonActionWrapper) {
|
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) {
|
if (action is ImportActionWrapper) {
|
||||||
showImportPanel(context, (document) {
|
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 {
|
class AddButtonActionWrapper extends ActionCell {
|
||||||
@ -87,6 +99,8 @@ class AddButtonActionWrapper extends ActionCell {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => pluginBuilder.menuName;
|
String get name => pluginBuilder.menuName;
|
||||||
|
|
||||||
|
PluginType get pluginType => pluginBuilder.pluginType;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImportActionWrapper extends ActionCell {
|
class ImportActionWrapper extends ActionCell {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#include "include/appflowy_backend/appflowy_flutter_backend_plugin.h"
|
|
||||||
|
|
||||||
// This must be included before many other Windows headers.
|
// This must be included before many other Windows headers.
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@ -13,69 +12,84 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include "include/appflowy_backend/app_flowy_backend_plugin.h"
|
||||||
|
|
||||||
namespace {
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
class AppFlowyBackendPlugin : public flutter::Plugin {
|
class AppFlowyBackendPlugin : public flutter::Plugin
|
||||||
public:
|
{
|
||||||
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
|
public:
|
||||||
|
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
|
||||||
|
|
||||||
AppFlowyBackendPlugin();
|
AppFlowyBackendPlugin();
|
||||||
|
|
||||||
virtual ~AppFlowyBackendPlugin();
|
virtual ~AppFlowyBackendPlugin();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Called when a method is called on this plugin's channel from Dart.
|
// Called when a method is called on this plugin's channel from Dart.
|
||||||
void HandleMethodCall(
|
void HandleMethodCall(
|
||||||
const flutter::MethodCall<flutter::EncodableValue> &method_call,
|
const flutter::MethodCall<flutter::EncodableValue> &method_call,
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||||
};
|
};
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void AppFlowyBackendPlugin::RegisterWithRegistrar(
|
void AppFlowyBackendPlugin::RegisterWithRegistrar(
|
||||||
flutter::PluginRegistrarWindows *registrar) {
|
flutter::PluginRegistrarWindows *registrar)
|
||||||
auto channel =
|
{
|
||||||
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
auto channel =
|
||||||
registrar->messenger(), "appflowy_backend",
|
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
||||||
&flutter::StandardMethodCodec::GetInstance());
|
registrar->messenger(), "appflowy_backend",
|
||||||
|
&flutter::StandardMethodCodec::GetInstance());
|
||||||
|
|
||||||
auto plugin = std::make_unique<AppFlowyBackendPlugin>();
|
auto plugin = std::make_unique<AppFlowyBackendPlugin>();
|
||||||
|
|
||||||
channel->SetMethodCallHandler(
|
channel->SetMethodCallHandler(
|
||||||
[plugin_pointer = plugin.get()](const auto &call, auto result) {
|
[plugin_pointer = plugin.get()](const auto &call, auto result)
|
||||||
plugin_pointer->HandleMethodCall(call, std::move(result));
|
{
|
||||||
});
|
plugin_pointer->HandleMethodCall(call, std::move(result));
|
||||||
|
});
|
||||||
|
|
||||||
registrar->AddPlugin(std::move(plugin));
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
} // 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(
|
void AppFlowyBackendPluginRegisterWithRegistrar(
|
||||||
FlutterDesktopPluginRegistrarRef registrar) {
|
FlutterDesktopPluginRegistrarRef registrar)
|
||||||
|
{
|
||||||
AppFlowyBackendPlugin::RegisterWithRegistrar(
|
AppFlowyBackendPlugin::RegisterWithRegistrar(
|
||||||
flutter::PluginRegistrarManager::GetInstance()
|
flutter::PluginRegistrarManager::GetInstance()
|
||||||
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
#include "appflowy_flutter_backend_plugin.h"
|
#include "appflowy_flutter_backend_plugin.h"
|
||||||
|
|
||||||
void AppFlowyBackendPluginCApiRegisterWithRegistrar(
|
void AppFlowyBackendPluginCApiRegisterWithRegistrar(
|
||||||
FlutterDesktopPluginRegistrarRef registrar) {
|
FlutterDesktopPluginRegistrarRef registrar)
|
||||||
appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(
|
{
|
||||||
flutter::PluginRegistrarManager::GetInstance()
|
appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(
|
||||||
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
flutter::PluginRegistrarManager::GetInstance()
|
||||||
|
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ class AppFlowyEditor extends StatefulWidget {
|
|||||||
this.toolbarItems = const [],
|
this.toolbarItems = const [],
|
||||||
this.editable = true,
|
this.editable = true,
|
||||||
this.autoFocus = false,
|
this.autoFocus = false,
|
||||||
|
this.focusedSelection,
|
||||||
this.customActionMenuBuilder,
|
this.customActionMenuBuilder,
|
||||||
ThemeData? themeData,
|
ThemeData? themeData,
|
||||||
}) : super(key: key) {
|
}) : 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.
|
/// Set the value to true to focus the editor on the start of the document.
|
||||||
final bool autoFocus;
|
final bool autoFocus;
|
||||||
|
final Selection? focusedSelection;
|
||||||
|
|
||||||
final Positioned Function(BuildContext context, List<ActionMenuItem> items)?
|
final Positioned Function(BuildContext context, List<ActionMenuItem> items)?
|
||||||
customActionMenuBuilder;
|
customActionMenuBuilder;
|
||||||
@ -89,7 +91,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
if (widget.editable && widget.autoFocus) {
|
if (widget.editable && widget.autoFocus) {
|
||||||
editorState.service.selectionService.updateSelection(
|
editorState.service.selectionService.updateSelection(
|
||||||
Selection.single(path: [0], startOffset: 0),
|
widget.focusedSelection ??
|
||||||
|
Selection.single(path: [0], startOffset: 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -12,3 +12,5 @@ export 'src/divider/divider_shortcut_event.dart';
|
|||||||
export 'src/emoji_picker/emoji_menu_item.dart';
|
export 'src/emoji_picker/emoji_menu_item.dart';
|
||||||
// Math Equation
|
// Math Equation
|
||||||
export 'src/math_ equation/math_equation_node_widget.dart';
|
export 'src/math_ equation/math_equation_node_widget.dart';
|
||||||
|
|
||||||
|
export 'src/extensions/theme_extension.dart';
|
||||||
|
Reference in New Issue
Block a user