Merge remote-tracking branch 'origin/main' into enzoftware/main

# Conflicts:
#	frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart
This commit is contained in:
Lucas.Xu 2022-10-08 10:15:30 +08:00
commit 017733856f
22 changed files with 364 additions and 58 deletions

View file

@ -1,3 +1,13 @@
## 0.0.6
* Add three plugins: Code Block, LateX, and Horizontal rule.
* Support web platform.
* Support more markdown syntax conversions.
* `~ ~` to format text as strikethrough
* `_ _` to format text as italic
* \` \` to format text as code
* `[]()` to format text as link
* Fix some bugs.
## 0.0.5
* Support customize the hotkeys for a shortcut on different platforms.
* Support customize a theme.

View file

@ -9,12 +9,6 @@
"align": "center"
}
},
{
"type": "tex",
"attributes": {
"tex": "x = 2"
}
},
{
"type": "text",
"attributes": { "subtype": "heading", "heading": "h1" },

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:example/plugin/code_block_node_widget.dart';
import 'package:example/plugin/horizontal_rule_node_widget.dart';
import 'package:example/plugin/tex_block_node_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -121,15 +122,18 @@ class _MyHomePageState extends State<MyHomePage> {
customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(),
'tex': TeXBlockNodeWidgetBuidler(),
'horizontal_rule': HorizontalRuleWidgetBuilder(),
},
shortcutEvents: [
enterInCodeBlock,
ignoreKeysInCodeBlock,
underscoreToItalic,
insertHorizontalRule,
],
selectionMenuItems: [
codeBlockItem,
codeBlockMenuItem,
teXBlockMenuItem,
horizontalRuleMenuItem,
],
),
);

View file

@ -44,9 +44,13 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
return KeyEventResult.ignored;
};
SelectionMenuItem codeBlockItem = SelectionMenuItem(
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
name: () => 'Code Block',
icon: const Icon(Icons.abc),
icon: const Icon(
Icons.abc,
color: Colors.black,
size: 18.0,
),
keywords: ['code block'],
handler: (editorState, _, __) {
final selection =

View file

@ -0,0 +1,167 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
ShortcutEvent insertHorizontalRule = ShortcutEvent(
key: 'Horizontal rule',
command: 'Minus',
handler: _insertHorzaontalRule,
);
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
final selection = editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (textNodes.length != 1 || selection == null) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
if (textNode.toRawString() == '--') {
TransactionBuilder(editorState)
..deleteText(textNode, 0, 2)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0)
..commit();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
name: () => 'Horizontal rule',
icon: const Icon(
Icons.horizontal_rule,
color: Colors.black,
size: 18.0,
),
keywords: ['horizontal rule'],
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (selection == null || textNodes.isEmpty) {
return;
}
final textNode = textNodes.first;
if (textNode.toRawString().isEmpty) {
TransactionBuilder(editorState)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0)
..commit();
} else {
TransactionBuilder(editorState)
..insertNode(
selection.end.path.next,
TextNode(
type: 'text',
children: LinkedList(),
attributes: {
'subtype': 'horizontal_rule',
},
delta: Delta()..insert('---'),
),
)
..afterSelection = selection
..commit();
}
},
);
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _HorizontalRuleWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return true;
};
}
class _HorizontalRuleWidget extends StatefulWidget {
const _HorizontalRuleWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
}
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
with SelectableMixin {
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Container(
height: 1,
color: Colors.grey,
),
);
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
bool get shouldCursorBlink => false;
@override
CursorStyle get cursorStyle => CursorStyle.borderLine;
@override
Rect? getCursorRectInPosition(Position position) {
final size = _renderBox.size;
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
}
@override
List<Rect> getRectsInSelection(Selection selection) =>
[Offset.zero & _renderBox.size];
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
}

View file

@ -6,7 +6,11 @@ import 'package:flutter_math_fork/flutter_math.dart';
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
name: () => 'Tex',
icon: const Icon(Icons.text_fields_rounded),
icon: const Icon(
Icons.text_fields_rounded,
color: Colors.black,
size: 18.0,
),
keywords: ['tex, latex, katex'],
handler: (editorState, _, __) {
final selection =

View file

@ -24,7 +24,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

View file

@ -0,0 +1,35 @@
{
"@@locale": "ml_IN",
"bold": "ബോൾഡ്",
"@bold": {},
"bulletedList": "ബുള്ളറ്റഡ് പട്ടിക",
"@bulletedList": {},
"checkbox": "ചെക്ക്ബോക്സ്",
"@checkbox": {},
"embedCode": "എംബെഡഡ് കോഡ്",
"@embedCode": {},
"heading1": "തലക്കെട്ട് 1",
"@heading1": {},
"heading2": "തലക്കെട്ട് 2",
"@heading2": {},
"heading3": "തലക്കെട്ട് 3",
"@heading3": {},
"highlight": "പ്രമുഖമാക്കിക്കാട്ടുക",
"@highlight": {},
"image": "ചിത്രം",
"@image": {},
"italic": "ഇറ്റാലിക്",
"@italic": {},
"link": "ലിങ്ക്",
"@link": {},
"numberedList": "അക്കമിട്ട പട്ടിക",
"@numberedList": {},
"quote": "ഉദ്ധരണി",
"@quote": {},
"strikethrough": "സ്ട്രൈക്ക്ത്രൂ",
"@strikethrough": {},
"text": "വചനം",
"@text": {},
"underline": "അടിവരയിടുക",
"@underline": {}
}

View file

@ -19,8 +19,9 @@ extension TextNodeExtension on TextNode {
}
final length = op.length;
if (start < endOffset && start + length > startOffset) {
if (op.attributes?.containsKey(styleKey) == true) {
return op.attributes![styleKey];
final attributes = op.attributes;
if (attributes != null && attributes[styleKey] is T?) {
return attributes[styleKey];
}
}
start += length;
@ -29,42 +30,40 @@ extension TextNodeExtension on TextNode {
}
bool allSatisfyLinkInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.href, <bool>(value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.href, (value) {
return value != null;
});
bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, <bool>(value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
return value == true;
});
bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.italic,
<bool>(value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.italic, (value) {
return value == true;
});
bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.underline,
<bool>(value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.underline, (value) {
return value == true;
});
bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.strikethrough,
<bool>(value) {
(value) {
return value == true;
});
bool allSatisfyCodeInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.code, <bool>(value) {
allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) {
return value == true;
});
bool allSatisfyInSelection(
Selection selection,
String styleKey,
bool Function<T>(T value) test,
bool Function(dynamic value) test,
) {
if (BuiltInAttributeKey.globalStyleKeys.contains(styleKey)) {
if (attributes.containsKey(styleKey)) {
@ -129,40 +128,40 @@ extension TextNodesExtension on List<TextNode> {
bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(
selection,
BuiltInAttributeKey.bold,
<bool>(value) => value == true,
(value) => value == true,
);
bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(
selection,
BuiltInAttributeKey.italic,
<bool>(value) => value == true,
(value) => value == true,
);
bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(
selection,
BuiltInAttributeKey.underline,
<bool>(value) => value == true,
(value) => value == true,
);
bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(
selection,
BuiltInAttributeKey.strikethrough,
<bool>(value) => value == true,
(value) => value == true,
);
bool allSatisfyInSelection(
Selection selection,
String styleKey,
bool Function<T>(T value) test,
bool Function(dynamic value) test,
) {
if (isEmpty) {
return false;
}
if (length == 1) {
return first.allSatisfyInSelection(selection, styleKey, <bool>(value) {
return first.allSatisfyInSelection(selection, styleKey, (value) {
return test(value);
});
} else {

View file

@ -26,6 +26,7 @@ import 'messages_hu-HU.dart' as messages_hu_hu;
import 'messages_id-ID.dart' as messages_id_id;
import 'messages_it-IT.dart' as messages_it_it;
import 'messages_ja-JP.dart' as messages_ja_jp;
import 'messages_ml_IN.dart' as messages_ml_in;
import 'messages_nl-NL.dart' as messages_nl_nl;
import 'messages_pl-PL.dart' as messages_pl_pl;
import 'messages_pt-BR.dart' as messages_pt_br;
@ -48,6 +49,7 @@ Map<String, LibraryLoader> _deferredLibraries = {
'id_ID': () => new Future.value(null),
'it_IT': () => new Future.value(null),
'ja_JP': () => new Future.value(null),
'ml_IN': () => new Future.value(null),
'nl_NL': () => new Future.value(null),
'pl_PL': () => new Future.value(null),
'pt_BR': () => new Future.value(null),
@ -82,6 +84,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
return messages_it_it.messages;
case 'ja_JP':
return messages_ja_jp.messages;
case 'ml_IN':
return messages_ml_in.messages;
case 'nl_NL':
return messages_nl_nl.messages;
case 'pl_PL':

View file

@ -0,0 +1,45 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ml_IN locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ml_IN';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage("ബോൾഡ്"),
"bulletedList":
MessageLookupByLibrary.simpleMessage("ബുള്ളറ്റഡ് പട്ടിക"),
"checkbox": MessageLookupByLibrary.simpleMessage("ചെക്ക്ബോക്സ്"),
"embedCode": MessageLookupByLibrary.simpleMessage("എംബെഡഡ് കോഡ്"),
"heading1": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 1"),
"heading2": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 2"),
"heading3": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 3"),
"highlight":
MessageLookupByLibrary.simpleMessage("പ്രമുഖമാക്കിക്കാട്ടുക"),
"image": MessageLookupByLibrary.simpleMessage("ചിത്രം"),
"italic": MessageLookupByLibrary.simpleMessage("ഇറ്റാലിക്"),
"link": MessageLookupByLibrary.simpleMessage("ലിങ്ക്"),
"numberedList":
MessageLookupByLibrary.simpleMessage("അക്കമിട്ട പട്ടിക"),
"quote": MessageLookupByLibrary.simpleMessage("ഉദ്ധരണി"),
"strikethrough": MessageLookupByLibrary.simpleMessage("സ്ട്രൈക്ക്ത്രൂ"),
"text": MessageLookupByLibrary.simpleMessage("വചനം"),
"underline": MessageLookupByLibrary.simpleMessage("അടിവരയിടുക")
};
}

View file

@ -229,6 +229,7 @@ class AppLocalizationDelegate
Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
Locale.fromSubtags(languageCode: 'ml', countryCode: 'IN'),
Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'),
Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart';
class CursorWidget extends StatefulWidget {
@ -9,9 +10,13 @@ class CursorWidget extends StatefulWidget {
required this.rect,
required this.color,
this.blinkingInterval = 0.5,
this.shouldBlink = true,
this.cursorStyle = CursorStyle.verticalLine,
}) : super(key: key);
final double blinkingInterval; // milliseconds
final bool shouldBlink;
final CursorStyle cursorStyle;
final Color color;
final Rect rect;
final LayerLink layerLink;
@ -67,11 +72,28 @@ class CursorWidgetState extends State<CursorWidget> {
// Ignore the gestures in cursor
// to solve the problem that cursor area cannot be selected.
child: IgnorePointer(
child: Container(
color: showCursor ? widget.color : Colors.transparent,
),
child: _buildCursor(context),
),
),
);
}
Widget _buildCursor(BuildContext context) {
var color = widget.color;
if (widget.shouldBlink && !showCursor) {
color = Colors.transparent;
}
switch (widget.cursorStyle) {
case CursorStyle.verticalLine:
return Container(
color: color,
);
case CursorStyle.borderLine:
return Container(
decoration: BoxDecoration(
border: Border.all(color: color, width: 2),
),
);
}
}
}

View file

@ -2,6 +2,11 @@ import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:flutter/material.dart';
enum CursorStyle {
verticalLine,
borderLine,
}
/// [SelectableMixin] is used for the editor to calculate the position
/// and size of the selection.
///
@ -53,4 +58,8 @@ mixin SelectableMixin<T extends StatefulWidget> on State<T> {
Selection? getWorldBoundaryInOffset(Offset start) {
return null;
}
bool get shouldCursorBlink => true;
CursorStyle get cursorStyle => CursorStyle.verticalLine;
}

View file

@ -10,7 +10,7 @@ typedef SelectionMenuItemHandler = void Function(
EditorState editorState,
SelectionMenuService menuService,
BuildContext context,
);
);
/// Selection Menu Item
class SelectionMenuItem {

View file

@ -73,7 +73,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.heading,
<bool>(value) => value == BuiltInAttributeKey.h1,
(value) => value == BuiltInAttributeKey.h1,
),
handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h1),
@ -90,7 +90,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.heading,
<bool>(value) => value == BuiltInAttributeKey.h2,
(value) => value == BuiltInAttributeKey.h2,
),
handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h2),
@ -107,7 +107,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.heading,
<bool>(value) => value == BuiltInAttributeKey.h3,
(value) => value == BuiltInAttributeKey.h3,
),
handler: (editorState, context) =>
formatHeading(editorState, BuiltInAttributeKey.h3),
@ -124,7 +124,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.bold,
<bool>(value) => value == true,
(value) => value == true,
),
handler: (editorState, context) => formatBold(editorState),
),
@ -140,7 +140,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.italic,
<bool>(value) => value == true,
(value) => value == true,
),
handler: (editorState, context) => formatItalic(editorState),
),
@ -156,7 +156,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.underline,
<bool>(value) => value == true,
(value) => value == true,
),
handler: (editorState, context) => formatUnderline(editorState),
),
@ -172,7 +172,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.strikethrough,
<bool>(value) => value == true,
(value) => value == true,
),
handler: (editorState, context) => formatStrikethrough(editorState),
),
@ -188,7 +188,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.code,
<bool>(value) => value == true,
(value) => value == true,
),
handler: (editorState, context) => formatEmbedCode(editorState),
),
@ -204,7 +204,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.subtype,
<bool>(value) => value == BuiltInAttributeKey.quote,
(value) => value == BuiltInAttributeKey.quote,
),
handler: (editorState, context) => formatQuote(editorState),
),
@ -220,7 +220,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.subtype,
<bool>(value) => value == BuiltInAttributeKey.bulletedList,
(value) => value == BuiltInAttributeKey.bulletedList,
),
handler: (editorState, context) => formatBulletedList(editorState),
),
@ -236,7 +236,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.href,
<bool>(value) => value != null,
(value) => value != null,
),
handler: (editorState, context) => showLinkMenu(context, editorState),
),
@ -252,7 +252,7 @@ List<ToolbarItem> defaultToolbarItems = [
highlightCallback: (editorState) => _allSatisfy(
editorState,
BuiltInAttributeKey.backgroundColor,
<bool>(value) => value != null,
(value) => value != null,
),
handler: (editorState, context) => formatHighlight(
editorState,
@ -284,7 +284,7 @@ ToolbarItemValidator _showInBuiltInTextSelection = (editorState) {
bool _allSatisfy(
EditorState editorState,
String styleKey,
bool Function<T>(T value) test,
bool Function(dynamic value) test,
) {
final selection = editorState.service.selectionService.currentSelection.value;
return selection != null &&

View file

@ -195,7 +195,7 @@ bool _allSatisfyInSelection(
return false;
}
return textNodes.allSatisfyInSelection(selection, styleKey, <bool>(value) {
return textNodes.allSatisfyInSelection(selection, styleKey, (value) {
return value == matchValue;
});
}

View file

@ -83,6 +83,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
}
} else {
if (textNodes.isEmpty) {
if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection =
Selection.collapsed(selection.start);
}
transactionBuilder.commit();
return KeyEventResult.handled;
}
final startPosition = selection.start;

View file

@ -457,6 +457,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
rect: cursorRect,
color: widget.cursorColor,
layerLink: node.layerLink,
shouldBlink: selectable.shouldCursorBlink,
cursorStyle: selectable.cursorStyle,
),
);

View file

@ -1,12 +1,13 @@
name: appflowy_editor
description: A highly customizable rich-text editor for Flutter
version: 0.0.5
version: 0.0.6
homepage: https://github.com/AppFlowy-IO/AppFlowy
platforms:
linux:
macos:
windows:
web:
environment:
sdk: ">=2.17.0 <3.0.0"

Some files were not shown because too many files have changed in this diff Show more