mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-14 09:27:55 +00:00
Merge pull request #1429 from LucasXu0/refactor_appflowy_editor_example
Refactor appflowy editor example
This commit is contained in:
commit
65f677b277
@ -1,5 +1,7 @@
|
|||||||
## 0.0.7
|
## 0.0.7
|
||||||
* Refactor theme customizer, and support dark mode.
|
* Refactor theme customizer, and support dark mode.
|
||||||
|
* Support export and import markdown.
|
||||||
|
* Refactor example project.
|
||||||
* Fix some bugs.
|
* Fix some bugs.
|
||||||
|
|
||||||
## 0.0.6
|
## 0.0.6
|
||||||
|
|||||||
@ -54,11 +54,9 @@ flutter pub get
|
|||||||
Start by creating a new empty AppFlowyEditor object.
|
Start by creating a new empty AppFlowyEditor object.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final editorStyle = EditorStyle.defaultStyle();
|
|
||||||
final editorState = EditorState.empty(); // an empty state
|
final editorState = EditorState.empty(); // an empty state
|
||||||
final editor = AppFlowyEditor(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editorStyle: editorStyle,
|
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -66,11 +64,9 @@ You can also create an editor from a JSON object in order to configure your init
|
|||||||
|
|
||||||
```dart
|
```dart
|
||||||
final json = ...;
|
final json = ...;
|
||||||
final editorStyle = EditorStyle.defaultStyle();
|
|
||||||
final editorState = EditorState(Document.fromJson(data));
|
final editorState = EditorState(Document.fromJson(data));
|
||||||
final editor = AppFlowyEditor(
|
final editor = AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editorStyle: editorStyle,
|
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -293,7 +293,6 @@ final editorState = EditorState(
|
|||||||
);
|
);
|
||||||
return AppFlowyEditor(
|
return AppFlowyEditor(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
editorStyle: EditorStyle.defaultStyle(),
|
|
||||||
shortcutEvents: const [],
|
shortcutEvents: const [],
|
||||||
customBuilders: {
|
customBuilders: {
|
||||||
'network_image': NetworkImageNodeWidgetBuilder(),
|
'network_image': NetworkImageNodeWidgetBuilder(),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"delta": [
|
"delta": [
|
||||||
{ "insert": "👋 " },
|
{ "insert": "👋 " },
|
||||||
{ "insert": "Welcome to ", "attributes": { "bold": true } },
|
{ "insert": "Welcome to", "attributes": { "bold": true } },
|
||||||
|
{ "insert": " " },
|
||||||
{
|
{
|
||||||
"insert": "AppFlowy Editor",
|
"insert": "AppFlowy Editor",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
@ -25,7 +26,8 @@
|
|||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"delta": [
|
"delta": [
|
||||||
{ "insert": "AppFlowy Editor is a " },
|
{ "insert": "AppFlowy Editor is a" },
|
||||||
|
{ "insert": " " },
|
||||||
{ "insert": "highly customizable", "attributes": { "bold": true } },
|
{ "insert": "highly customizable", "attributes": { "bold": true } },
|
||||||
{ "insert": " " },
|
{ "insert": " " },
|
||||||
{ "insert": "rich-text editor", "attributes": { "italic": true } },
|
{ "insert": "rich-text editor", "attributes": { "italic": true } },
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 245 KiB |
@ -0,0 +1,328 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:example/pages/simple_editor.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:universal_html/html.dart' as html;
|
||||||
|
|
||||||
|
enum ExportFileType {
|
||||||
|
json,
|
||||||
|
markdown,
|
||||||
|
html,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on ExportFileType {
|
||||||
|
String get extension {
|
||||||
|
switch (this) {
|
||||||
|
case ExportFileType.json:
|
||||||
|
return 'json';
|
||||||
|
case ExportFileType.markdown:
|
||||||
|
return 'md';
|
||||||
|
case ExportFileType.html:
|
||||||
|
return 'html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomePage extends StatefulWidget {
|
||||||
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
final _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
late WidgetBuilder _widgetBuilder;
|
||||||
|
late EditorState _editorState;
|
||||||
|
late Future<String> _jsonString;
|
||||||
|
ThemeData _themeData = ThemeData.light().copyWith(
|
||||||
|
extensions: [
|
||||||
|
...lightEditorStyleExtension,
|
||||||
|
...lightPlguinStyleExtension,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_jsonString = rootBundle.loadString('assets/example.json');
|
||||||
|
_widgetBuilder = (context) => SimpleEditor(
|
||||||
|
jsonString: _jsonString,
|
||||||
|
themeData: _themeData,
|
||||||
|
onEditorStateChange: (editorState) {
|
||||||
|
_editorState = editorState;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
drawer: _buildDrawer(context),
|
||||||
|
body: _buildBody(context),
|
||||||
|
floatingActionButton: _buildFloatingActionButton(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDrawer(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
DrawerHeader(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/icon.png',
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// AppFlowy Editor Demo
|
||||||
|
_buildSeparator(context, 'AppFlowy Editor Demo'),
|
||||||
|
_buildListTile(context, 'With Example.json', () {
|
||||||
|
final jsonString = rootBundle.loadString('assets/example.json');
|
||||||
|
_loadEditor(context, jsonString);
|
||||||
|
}),
|
||||||
|
_buildListTile(context, 'With Empty Document', () {
|
||||||
|
final jsonString = Future<String>.value(
|
||||||
|
jsonEncode(EditorState.empty().document.toJson()).toString(),
|
||||||
|
);
|
||||||
|
_loadEditor(context, jsonString);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Encoder Demo
|
||||||
|
_buildSeparator(context, 'Encoder Demo'),
|
||||||
|
_buildListTile(context, 'Export To JSON', () {
|
||||||
|
_exportFile(_editorState, ExportFileType.json);
|
||||||
|
}),
|
||||||
|
_buildListTile(context, 'Export to Markdown', () {
|
||||||
|
_exportFile(_editorState, ExportFileType.markdown);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Decoder Demo
|
||||||
|
_buildSeparator(context, 'Decoder Demo'),
|
||||||
|
_buildListTile(context, 'Import From JSON', () {
|
||||||
|
_importFile(ExportFileType.json);
|
||||||
|
}),
|
||||||
|
_buildListTile(context, 'Import From Markdown', () {
|
||||||
|
_importFile(ExportFileType.markdown);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Theme Demo
|
||||||
|
_buildSeparator(context, 'Theme Demo'),
|
||||||
|
_buildListTile(context, 'Bulit In Dark Mode', () {
|
||||||
|
_jsonString = Future<String>.value(
|
||||||
|
jsonEncode(_editorState.document.toJson()).toString(),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_themeData = ThemeData.dark().copyWith(
|
||||||
|
extensions: [
|
||||||
|
...darkEditorStyleExtension,
|
||||||
|
...darkPlguinStyleExtension,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
_buildListTile(context, 'Custom Theme', () {
|
||||||
|
_jsonString = Future<String>.value(
|
||||||
|
jsonEncode(_editorState.document.toJson()).toString(),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_themeData = _customizeEditorTheme(context);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody(BuildContext context) {
|
||||||
|
return _widgetBuilder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListTile(
|
||||||
|
BuildContext context,
|
||||||
|
String text,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
) {
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 16),
|
||||||
|
title: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
onTap?.call();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSeparator(BuildContext context, String text) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFloatingActionButton(BuildContext context) {
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
_scaffoldKey.currentState?.openDrawer();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.menu),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadEditor(BuildContext context, Future<String> jsonString) {
|
||||||
|
_jsonString = jsonString;
|
||||||
|
setState(
|
||||||
|
() {
|
||||||
|
_widgetBuilder = (context) => SimpleEditor(
|
||||||
|
jsonString: _jsonString,
|
||||||
|
themeData: _themeData,
|
||||||
|
onEditorStateChange: (editorState) {
|
||||||
|
_editorState = editorState;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exportFile(
|
||||||
|
EditorState editorState,
|
||||||
|
ExportFileType fileType,
|
||||||
|
) async {
|
||||||
|
var result = '';
|
||||||
|
|
||||||
|
switch (fileType) {
|
||||||
|
case ExportFileType.json:
|
||||||
|
result = jsonEncode(editorState.document.toJson());
|
||||||
|
break;
|
||||||
|
case ExportFileType.markdown:
|
||||||
|
result = documentToMarkdown(editorState.document);
|
||||||
|
break;
|
||||||
|
case ExportFileType.html:
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kIsWeb) {
|
||||||
|
final path = await FilePicker.platform.saveFile(
|
||||||
|
fileName: 'document.${fileType.extension}',
|
||||||
|
);
|
||||||
|
if (path != null) {
|
||||||
|
await File(path).writeAsString(result);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('This document is saved to the $path'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final blob = html.Blob([result], 'text/plain', 'native');
|
||||||
|
html.AnchorElement(
|
||||||
|
href: html.Url.createObjectUrlFromBlob(blob).toString(),
|
||||||
|
)
|
||||||
|
..setAttribute('download', 'document.${fileType.extension}')
|
||||||
|
..click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _importFile(ExportFileType fileType) async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
allowMultiple: false,
|
||||||
|
allowedExtensions: [fileType.extension],
|
||||||
|
type: FileType.custom,
|
||||||
|
);
|
||||||
|
var plainText = '';
|
||||||
|
if (!kIsWeb) {
|
||||||
|
final path = result?.files.single.path;
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plainText = await File(path).readAsString();
|
||||||
|
} else {
|
||||||
|
final bytes = result?.files.first.bytes;
|
||||||
|
if (bytes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plainText = const Utf8Decoder().convert(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonString = '';
|
||||||
|
switch (fileType) {
|
||||||
|
case ExportFileType.json:
|
||||||
|
jsonString = jsonEncode(plainText);
|
||||||
|
break;
|
||||||
|
case ExportFileType.markdown:
|
||||||
|
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
|
||||||
|
break;
|
||||||
|
case ExportFileType.html:
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
_loadEditor(context, Future<String>.value(jsonString));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData _customizeEditorTheme(BuildContext context) {
|
||||||
|
final dark = EditorStyle.dark;
|
||||||
|
final editorStyle = dark.copyWith(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 150),
|
||||||
|
cursorColor: Colors.blue.shade600,
|
||||||
|
selectionColor: Colors.yellow.shade600.withOpacity(0.5),
|
||||||
|
textStyle: GoogleFonts.poppins().copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
placeholderTextStyle: GoogleFonts.poppins().copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
code: dark.code?.copyWith(
|
||||||
|
backgroundColor: Colors.lightBlue.shade200,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
highlightColorHex: '0x60FF0000', // red
|
||||||
|
);
|
||||||
|
|
||||||
|
final quote = QuotedTextPluginStyle.dark.copyWith(
|
||||||
|
textStyle: (_, __) => GoogleFonts.poppins().copyWith(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.blue.shade400,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Theme.of(context).copyWith(extensions: [
|
||||||
|
editorStyle,
|
||||||
|
...darkPlguinStyleExtension,
|
||||||
|
quote,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,23 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'package:example/home_page.dart';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:example/plugin/editor_theme.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
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:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:universal_html/html.dart' as html;
|
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
import 'expandable_floating_action_button.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
@ -50,201 +37,8 @@ class MyHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
int _pageIndex = 0;
|
|
||||||
EditorState? _editorState;
|
|
||||||
bool darkMode = false;
|
|
||||||
Future<String>? _jsonString;
|
|
||||||
|
|
||||||
ThemeData? _editorThemeData;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return const HomePage();
|
||||||
extendBodyBehindAppBar: true,
|
|
||||||
body: _buildEditor(context),
|
|
||||||
floatingActionButton: _buildExpandableFab(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEditor(BuildContext context) {
|
|
||||||
if (_jsonString != null) {
|
|
||||||
return _buildEditorWithJsonString(_jsonString!);
|
|
||||||
}
|
|
||||||
if (_pageIndex == 0) {
|
|
||||||
return _buildEditorWithJsonString(
|
|
||||||
rootBundle.loadString('assets/example.json'),
|
|
||||||
);
|
|
||||||
} else if (_pageIndex == 1) {
|
|
||||||
return _buildEditorWithJsonString(
|
|
||||||
Future.value(
|
|
||||||
jsonEncode(EditorState.empty().document.toJson()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEditorWithJsonString(Future<String> jsonString) {
|
|
||||||
return FutureBuilder<String>(
|
|
||||||
future: jsonString,
|
|
||||||
builder: (_, snapshot) {
|
|
||||||
if (snapshot.hasData &&
|
|
||||||
snapshot.connectionState == ConnectionState.done) {
|
|
||||||
_editorState ??= EditorState(
|
|
||||||
document: Document.fromJson(
|
|
||||||
Map<String, Object>.from(
|
|
||||||
json.decode(snapshot.data!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_editorState!.logConfiguration
|
|
||||||
..level = LogLevel.all
|
|
||||||
..handler = (message) {
|
|
||||||
debugPrint(message);
|
|
||||||
};
|
|
||||||
_editorState!.transactionStream.listen((event) {
|
|
||||||
debugPrint('Transaction: ${event.toJson()}');
|
|
||||||
});
|
|
||||||
_editorThemeData ??= Theme.of(context).copyWith(extensions: [
|
|
||||||
if (darkMode) ...darkEditorStyleExtension,
|
|
||||||
if (darkMode) ...darkPlguinStyleExtension,
|
|
||||||
if (!darkMode) ...lightEditorStyleExtension,
|
|
||||||
if (!darkMode) ...lightPlguinStyleExtension,
|
|
||||||
]);
|
|
||||||
return Container(
|
|
||||||
color: darkMode ? Colors.black : Colors.white,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: AppFlowyEditor(
|
|
||||||
editorState: _editorState!,
|
|
||||||
editable: true,
|
|
||||||
autoFocus: _editorState!.document.isEmpty,
|
|
||||||
themeData: _editorThemeData,
|
|
||||||
customBuilders: {
|
|
||||||
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
|
||||||
'tex': TeXBlockNodeWidgetBuidler(),
|
|
||||||
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
|
||||||
},
|
|
||||||
shortcutEvents: [
|
|
||||||
enterInCodeBlock,
|
|
||||||
ignoreKeysInCodeBlock,
|
|
||||||
insertHorizontalRule,
|
|
||||||
],
|
|
||||||
selectionMenuItems: [
|
|
||||||
codeBlockMenuItem,
|
|
||||||
teXBlockMenuItem,
|
|
||||||
horizontalRuleMenuItem,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildExpandableFab() {
|
|
||||||
return ExpandableFab(
|
|
||||||
distance: 112.0,
|
|
||||||
children: [
|
|
||||||
ActionButton(
|
|
||||||
icon: const Icon(Icons.abc),
|
|
||||||
onPressed: () => _switchToPage(0),
|
|
||||||
),
|
|
||||||
ActionButton(
|
|
||||||
icon: const Icon(Icons.abc),
|
|
||||||
onPressed: () => _switchToPage(1),
|
|
||||||
),
|
|
||||||
ActionButton(
|
|
||||||
icon: const Icon(Icons.print),
|
|
||||||
onPressed: () => _exportDocument(_editorState!),
|
|
||||||
),
|
|
||||||
ActionButton(
|
|
||||||
icon: const Icon(Icons.import_export),
|
|
||||||
onPressed: () async => await _importDocument(),
|
|
||||||
),
|
|
||||||
ActionButton(
|
|
||||||
icon: const Icon(Icons.dark_mode),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
darkMode = !darkMode;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ActionButton(
|
|
||||||
icon: const Icon(Icons.color_lens),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_editorThemeData = customizeEditorTheme(context);
|
|
||||||
darkMode = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exportDocument(EditorState editorState) async {
|
|
||||||
final document = editorState.document.toJson();
|
|
||||||
final json = jsonEncode(document);
|
|
||||||
if (kIsWeb) {
|
|
||||||
final blob = html.Blob([json], 'text/plain', 'native');
|
|
||||||
html.AnchorElement(
|
|
||||||
href: html.Url.createObjectUrlFromBlob(blob).toString(),
|
|
||||||
)
|
|
||||||
..setAttribute('download', 'editor.json')
|
|
||||||
..click();
|
|
||||||
} else {
|
|
||||||
final directory = await getTemporaryDirectory();
|
|
||||||
final path = directory.path;
|
|
||||||
final file = File('$path/editor.json');
|
|
||||||
await file.writeAsString(json);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('The document is saved to the ${file.path}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _importDocument() async {
|
|
||||||
if (kIsWeb) {
|
|
||||||
final result = await FilePicker.platform.pickFiles(
|
|
||||||
allowMultiple: false,
|
|
||||||
allowedExtensions: ['json'],
|
|
||||||
type: FileType.custom,
|
|
||||||
);
|
|
||||||
final bytes = result?.files.first.bytes;
|
|
||||||
if (bytes != null) {
|
|
||||||
final jsonString = const Utf8Decoder().convert(bytes);
|
|
||||||
setState(() {
|
|
||||||
_editorState = null;
|
|
||||||
_jsonString = Future.value(jsonString);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final directory = await getTemporaryDirectory();
|
|
||||||
final path = '${directory.path}/editor.json';
|
|
||||||
final file = File(path);
|
|
||||||
setState(() {
|
|
||||||
_editorState = null;
|
|
||||||
_jsonString = file.readAsString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _switchToPage(int pageIndex) {
|
|
||||||
if (pageIndex != _pageIndex) {
|
|
||||||
setState(() {
|
|
||||||
_editorThemeData = null;
|
|
||||||
_editorState = null;
|
|
||||||
_pageIndex = pageIndex;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SimpleEditor extends StatelessWidget {
|
||||||
|
const SimpleEditor({
|
||||||
|
super.key,
|
||||||
|
required this.jsonString,
|
||||||
|
required this.themeData,
|
||||||
|
required this.onEditorStateChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Future<String> jsonString;
|
||||||
|
final ThemeData themeData;
|
||||||
|
final void Function(EditorState editorState) onEditorStateChange;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: jsonString,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData &&
|
||||||
|
snapshot.connectionState == ConnectionState.done) {
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: Document.fromJson(
|
||||||
|
Map<String, Object>.from(
|
||||||
|
json.decode(snapshot.data!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onEditorStateChange(editorState);
|
||||||
|
return AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
themeData: themeData,
|
||||||
|
autoFocus: editorState.document.isEmpty,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -70,8 +70,7 @@ flutter:
|
|||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- example.json
|
- example.json
|
||||||
- big_document.json
|
- assets/images/icon.png
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|||||||
@ -115,6 +115,10 @@ class Document {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.children.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final node = root.children.first;
|
final node = root.children.first;
|
||||||
if (node is TextNode &&
|
if (node is TextNode &&
|
||||||
(node.delta.isEmpty || node.delta.toPlainText().isEmpty)) {
|
(node.delta.isEmpty || node.delta.toPlainText().isEmpty)) {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
|
|||||||
class EditorStyle extends ThemeExtension<EditorStyle> {
|
class EditorStyle extends ThemeExtension<EditorStyle> {
|
||||||
// Editor styles
|
// Editor styles
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
|
final Color? backgroundColor;
|
||||||
final Color? cursorColor;
|
final Color? cursorColor;
|
||||||
final Color? selectionColor;
|
final Color? selectionColor;
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||||||
|
|
||||||
EditorStyle({
|
EditorStyle({
|
||||||
required this.padding,
|
required this.padding,
|
||||||
|
required this.backgroundColor,
|
||||||
required this.cursorColor,
|
required this.cursorColor,
|
||||||
required this.selectionColor,
|
required this.selectionColor,
|
||||||
required this.selectionMenuBackgroundColor,
|
required this.selectionMenuBackgroundColor,
|
||||||
@ -63,6 +65,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||||||
@override
|
@override
|
||||||
EditorStyle copyWith({
|
EditorStyle copyWith({
|
||||||
EdgeInsets? padding,
|
EdgeInsets? padding,
|
||||||
|
Color? backgroundColor,
|
||||||
Color? cursorColor,
|
Color? cursorColor,
|
||||||
Color? selectionColor,
|
Color? selectionColor,
|
||||||
Color? selectionMenuBackgroundColor,
|
Color? selectionMenuBackgroundColor,
|
||||||
@ -84,6 +87,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||||||
}) {
|
}) {
|
||||||
return EditorStyle(
|
return EditorStyle(
|
||||||
padding: padding ?? this.padding,
|
padding: padding ?? this.padding,
|
||||||
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||||
cursorColor: cursorColor ?? this.cursorColor,
|
cursorColor: cursorColor ?? this.cursorColor,
|
||||||
selectionColor: selectionColor ?? this.selectionColor,
|
selectionColor: selectionColor ?? this.selectionColor,
|
||||||
selectionMenuBackgroundColor:
|
selectionMenuBackgroundColor:
|
||||||
@ -120,6 +124,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||||||
}
|
}
|
||||||
return EditorStyle(
|
return EditorStyle(
|
||||||
padding: EdgeInsets.lerp(padding, other.padding, t),
|
padding: EdgeInsets.lerp(padding, other.padding, t),
|
||||||
|
backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t),
|
||||||
cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
|
cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
|
||||||
textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
|
textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
|
||||||
selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
|
selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
|
||||||
@ -155,6 +160,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||||||
|
|
||||||
static final light = EditorStyle(
|
static final light = EditorStyle(
|
||||||
padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
|
padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
cursorColor: const Color(0xFF00BCF0),
|
cursorColor: const Color(0xFF00BCF0),
|
||||||
selectionColor: const Color.fromARGB(53, 111, 201, 231),
|
selectionColor: const Color.fromARGB(53, 111, 201, 231),
|
||||||
selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
|
selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
|
||||||
@ -184,6 +190,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static final dark = light.copyWith(
|
static final dark = light.copyWith(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
textStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
|
textStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
|
||||||
placeholderTextStyle: TextStyle(
|
placeholderTextStyle: TextStyle(
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
|
|||||||
@ -119,7 +119,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
data: widget.themeData,
|
data: widget.themeData,
|
||||||
child: AppFlowyScroll(
|
child: AppFlowyScroll(
|
||||||
key: editorState.service.scrollServiceKey,
|
key: editorState.service.scrollServiceKey,
|
||||||
child: Padding(
|
child: Container(
|
||||||
|
color: editorStyle.backgroundColor,
|
||||||
padding: editorStyle.padding!,
|
padding: editorStyle.padding!,
|
||||||
child: AppFlowySelection(
|
child: AppFlowySelection(
|
||||||
key: editorState.service.selectionServiceKey,
|
key: editorState.service.selectionServiceKey,
|
||||||
|
|||||||
@ -252,6 +252,31 @@ Delta _lineContentToDelta(String lineContent) {
|
|||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _pasteMarkdown(EditorState editorState, String markdown) {
|
||||||
|
final selection =
|
||||||
|
editorState.service.selectionService.currentSelection.value?.normalized;
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lines = markdown.split('\n');
|
||||||
|
|
||||||
|
if (lines.length == 1) {
|
||||||
|
_pasteSingleLine(editorState, selection, lines[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = selection.end.path.next;
|
||||||
|
final node = editorState.document.nodeAtPath(selection.end.path);
|
||||||
|
if (node is TextNode && node.toPlainText().isEmpty) {
|
||||||
|
path = selection.end.path;
|
||||||
|
}
|
||||||
|
final document = markdownToDocument(markdown);
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.insertNodes(path, document.root.children);
|
||||||
|
editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
void _handlePastePlainText(EditorState editorState, String plainText) {
|
void _handlePastePlainText(EditorState editorState, String plainText) {
|
||||||
final selection = editorState.cursorSelection?.normalized;
|
final selection = editorState.cursorSelection?.normalized;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
@ -269,45 +294,7 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
|
|||||||
// single line
|
// single line
|
||||||
_pasteSingleLine(editorState, selection, lines.first);
|
_pasteSingleLine(editorState, selection, lines.first);
|
||||||
} else {
|
} else {
|
||||||
final firstLine = lines[0];
|
_pasteMarkdown(editorState, plainText);
|
||||||
final beginOffset = selection.end.offset;
|
|
||||||
final remains = lines.sublist(1);
|
|
||||||
|
|
||||||
final path = [...selection.end.path];
|
|
||||||
if (path.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final node =
|
|
||||||
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
|
||||||
final insertedLineSuffix = node.delta.slice(beginOffset);
|
|
||||||
|
|
||||||
path[path.length - 1]++;
|
|
||||||
final tb = editorState.transaction;
|
|
||||||
final List<TextNode> nodes =
|
|
||||||
remains.map((e) => TextNode(delta: _lineContentToDelta(e))).toList();
|
|
||||||
|
|
||||||
final afterSelection =
|
|
||||||
_computeSelectionAfterPasteMultipleNodes(editorState, nodes);
|
|
||||||
|
|
||||||
// append remain text to the last line
|
|
||||||
if (nodes.isNotEmpty) {
|
|
||||||
final last = nodes.last;
|
|
||||||
nodes[nodes.length - 1] =
|
|
||||||
TextNode(delta: last.delta..addAll(insertedLineSuffix));
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert first line
|
|
||||||
tb.updateText(
|
|
||||||
node,
|
|
||||||
Delta()
|
|
||||||
..retain(beginOffset)
|
|
||||||
..insert(firstLine)
|
|
||||||
..delete(node.delta.length - beginOffset));
|
|
||||||
// insert remains
|
|
||||||
tb.insertNodes(path, nodes);
|
|
||||||
tb.afterSelection = afterSelection;
|
|
||||||
editorState.apply(tb);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user