import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:example/pages/customize_theme_for_editor.dart'; import 'package:example/pages/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:universal_html/html.dart' as html; enum ExportFileType { documentJson, markdown, html, delta, } extension on ExportFileType { String get extension { switch (this) { case ExportFileType.documentJson: case ExportFileType.delta: 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 createState() => _HomePageState(); } class _HomePageState extends State { final _scaffoldKey = GlobalKey(); late WidgetBuilder _widgetBuilder; late EditorState _editorState; late Future _jsonString; @override void initState() { super.initState(); _jsonString = rootBundle.loadString('assets/example.json'); _widgetBuilder = (context) => Editor( jsonString: _jsonString, onEditorStateChange: (editorState) { _editorState = editorState; }, ); } @override void reassemble() { super.reassemble(); _widgetBuilder = (context) => Editor( jsonString: _jsonString, onEditorStateChange: (editorState) { _editorState = editorState; _jsonString = Future.value( jsonEncode(_editorState.document.toJson()), ); }, ); } @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, extendBodyBehindAppBar: PlatformExtension.isDesktopOrWeb, drawer: _buildDrawer(context), appBar: AppBar( backgroundColor: const Color.fromARGB(255, 134, 46, 247), foregroundColor: Colors.white, surfaceTintColor: Colors.transparent, title: const Text('AppFlowy Editor'), ), body: SafeArea(child: _buildBody(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.jpeg', 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 Large Document (10000+ lines)', () { final nodes = List.generate( 10000, (index) => paragraphNode(text: '$index ${generateRandomString(50)}'), ); final editorState = EditorState( document: Document(root: pageNode(children: nodes)), ); final jsonString = Future.value( jsonEncode(editorState.document.toJson()), ); _loadEditor(context, jsonString); }), _buildListTile(context, 'With Example.html', () async { final htmlString = await rootBundle.loadString('assets/example.html'); final html = htmlToDocument(htmlString); // final html = HTMLToNodesConverter(htmlString).toDocument(); final jsonString = Future.value( jsonEncode( html.toJson(), ).toString(), ); if (mounted) { _loadEditor(context, jsonString); } }), _buildListTile(context, 'With Empty Document', () { final jsonString = Future.value( jsonEncode( EditorState.blank(withInitialText: true).document.toJson(), ).toString(), ); _loadEditor(context, jsonString); }), // Theme Demo _buildSeparator(context, 'Theme Demo'), _buildListTile(context, 'Custom Theme', () { Navigator.push( context, MaterialPageRoute( builder: (context) => const CustomizeThemeForEditor(), ), ); }), // Encoder Demo _buildSeparator(context, 'Export To X Demo'), _buildListTile(context, 'Export To JSON', () { _exportFile(_editorState, ExportFileType.documentJson); }), _buildListTile(context, 'Export to Markdown', () { _exportFile(_editorState, ExportFileType.markdown); }), // Decoder Demo _buildSeparator(context, 'Import From X Demo'), _buildListTile(context, 'Import From Document JSON', () { _importFile(ExportFileType.documentJson); }), _buildListTile(context, 'Import From Markdown', () { _importFile(ExportFileType.markdown); }), _buildListTile(context, 'Import From Quill Delta', () { _importFile(ExportFileType.delta); }), ], ), ); } 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, ), ), ); } Future _loadEditor( BuildContext context, Future jsonString, ) async { final completer = Completer(); _jsonString = jsonString; setState( () { _widgetBuilder = (context) => Editor( jsonString: _jsonString, onEditorStateChange: (editorState) { _editorState = editorState; }, ); }, ); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { completer.complete(); }); return completer.future; } void _exportFile( EditorState editorState, ExportFileType fileType, ) async { var result = ''; switch (fileType) { case ExportFileType.documentJson: result = jsonEncode(editorState.document.toJson()); break; case ExportFileType.markdown: result = documentToMarkdown(editorState.document); break; case ExportFileType.html: case ExportFileType.delta: 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.documentJson: jsonString = plainText; break; case ExportFileType.markdown: jsonString = jsonEncode(markdownToDocument(plainText).toJson()); break; case ExportFileType.delta: final delta = Delta.fromJson(jsonDecode(plainText)); final document = quillDeltaEncoder.convert(delta); jsonString = jsonEncode(document.toJson()); break; case ExportFileType.html: throw UnimplementedError(); } if (mounted) { _loadEditor(context, Future.value(jsonString)); } } } String generateRandomString(int len) { var r = Random(); return String.fromCharCodes( List.generate(len, (index) => r.nextInt(33) + 89), ); }