appflowy-editor/example/lib/home_page.dart

515 lines
15 KiB
Dart
Raw Normal View History

2023-01-07 09:38:38 +08:00
import 'dart:async';
import 'dart:convert';
import 'dart:io' if (dart.library.html) 'package:example/file_io_stub.dart';
2023-09-11 15:03:14 +08:00
import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart';
2025-05-07 10:28:18 +08:00
import 'package:example/pages/animated_markdown_page.dart';
import 'package:example/pages/auto_complete_editor.dart';
import 'package:example/pages/auto_expand_editor.dart';
import 'package:example/pages/collab_editor.dart';
import 'package:example/pages/collab_editor_offline.dart';
import 'package:example/pages/collab_selection_editor.dart';
2023-07-07 16:07:47 +08:00
import 'package:example/pages/customize_theme_for_editor.dart';
import 'package:example/pages/drag_to_reorder_editor.dart';
2023-07-07 16:07:47 +08:00
import 'package:example/pages/editor.dart';
import 'package:example/pages/editor_list.dart';
import 'package:example/pages/fixed_toolbar_editor.dart';
import 'package:example/pages/focus_example_for_editor.dart';
import 'package:example/pages/inline_comment_editor.dart';
import 'package:example/pages/markdown_editor.dart';
import 'package:example/pages/ten_thousand_links_showcase.dart';
2022-11-09 11:50:08 +08:00
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:printing/printing.dart';
2022-11-09 11:50:08 +08:00
import 'package:universal_html/html.dart' as html;
import 'package:universal_platform/universal_platform.dart';
enum ExportFileType {
2023-07-03 19:19:32 +08:00
documentJson,
markdown,
pdf,
delta,
}
extension on ExportFileType {
String get extension {
switch (this) {
2023-07-03 19:19:32 +08:00
case ExportFileType.documentJson:
case ExportFileType.delta:
return 'json';
case ExportFileType.markdown:
return 'md';
case ExportFileType.pdf:
return 'pdf';
}
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
2023-07-07 16:07:47 +08:00
late WidgetBuilder _widgetBuilder;
late EditorState _editorState;
late Future<String> _jsonString;
@override
void initState() {
super.initState();
_jsonString = UniversalPlatform.isDesktopOrWeb
? rootBundle.loadString('assets/example.json')
: rootBundle.loadString('assets/mobile_example.json');
2023-09-11 15:03:14 +08:00
2023-07-07 16:07:47 +08:00
_widgetBuilder = (context) => Editor(
jsonString: _jsonString,
onEditorStateChange: (editorState) {
_editorState = editorState;
},
);
}
@override
void reassemble() {
super.reassemble();
2023-07-07 16:07:47 +08:00
_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: UniversalPlatform.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'),
),
2024-06-11 10:15:15 +08:00
body: SafeArea(
maintainBottomViewPadding: true,
child: _widgetBuilder(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 = UniversalPlatform.isDesktopOrWeb
? rootBundle.loadString('assets/example.json')
: rootBundle.loadString('assets/mobile_example.json');
2022-11-09 11:50:08 +08:00
_loadEditor(context, jsonString);
}),
2023-09-11 15:03:14 +08:00
_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);
}),
2023-05-31 16:05:49 +08:00
_buildListTile(context, 'With Example.html', () async {
final htmlString =
await rootBundle.loadString('assets/example.html');
final html = htmlToDocument(htmlString);
final jsonString = Future<String>.value(
jsonEncode(
html.toJson(),
).toString(),
);
2024-02-28 10:03:29 +07:00
if (context.mounted) {
_loadEditor(context, jsonString);
}
2023-05-31 16:05:49 +08:00
}),
_buildListTile(context, 'With Empty Document', () {
final jsonString = Future<String>.value(
jsonEncode(
EditorState.blank(withInitialText: true).document.toJson(),
).toString(),
);
2022-11-09 11:50:08 +08:00
_loadEditor(context, jsonString);
}),
2023-07-07 16:07:47 +08:00
// Theme Demo
_buildSeparator(context, 'Showcases'),
_buildListTile(context, 'Drag to reorder', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DragToReorderEditor(),
),
);
}),
_buildListTile(context, 'Markdown Editor', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const MarkdownEditor(),
),
);
}),
2025-05-07 10:28:18 +08:00
_buildListTile(context, 'Animated Markdown Editor', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AnimatedMarkdownPage(),
),
);
}),
_buildListTile(context, 'Auto complete Editor', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AutoCompleteEditor(),
),
);
}),
_buildListTile(context, 'Collab Editor', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CollabEditor(),
),
);
}),
_buildListTile(context, 'Collab Selection', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CollabSelectionEditor(),
),
);
}),
_buildListTile(context, 'Collab Offline', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CollabEditorOffline(),
),
);
}),
2023-07-07 16:07:47 +08:00
_buildListTile(context, 'Custom Theme', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CustomizeThemeForEditor(),
),
);
}),
_buildListTile(context, 'RTL', () {
final jsonString = rootBundle.loadString(
'assets/arabic_example.json',
);
_loadEditor(
context,
jsonString,
textDirection: TextDirection.rtl,
);
}),
_buildListTile(context, 'Focus Example', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const FocusExampleForEditor(),
),
);
}),
_buildListTile(context, 'Editor List', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EditorList(),
),
);
}),
_buildListTile(context, 'Fixed Toolbar', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const FixedToolbarExample(),
),
);
}),
2023-07-07 16:07:47 +08:00
_buildListTile(context, 'Auto Expand Editor', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AutoExpandEditor(
editorState: EditorState.blank(),
),
),
);
}),
_buildListTile(context, 'Inline Comment', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const InlineCommentEditor(),
),
);
}),
_buildListTile(context, '10,000 Links (one link per paragraph)', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TenThousandLinksShowcase(),
),
);
}),
// Encoder Demo
2023-07-03 19:19:32 +08:00
_buildSeparator(context, 'Export To X Demo'),
2022-11-09 11:50:08 +08:00
_buildListTile(context, 'Export To JSON', () {
2023-07-03 19:19:32 +08:00
_exportFile(_editorState, ExportFileType.documentJson);
}),
_buildListTile(context, 'Export to Markdown', () {
_exportFile(_editorState, ExportFileType.markdown);
2022-11-09 11:50:08 +08:00
}),
_buildListTile(context, 'Export to PDF', () {
_exportFile(_editorState, ExportFileType.pdf);
}),
// Decoder Demo
2023-07-03 19:19:32 +08:00
_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 _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,
),
),
);
}
2023-01-07 09:38:38 +08:00
Future<void> _loadEditor(
BuildContext context,
Future<String> jsonString, {
TextDirection textDirection = TextDirection.ltr,
}) async {
2023-01-07 09:38:38 +08:00
final completer = Completer<void>();
_jsonString = jsonString;
setState(
() {
2023-07-07 16:07:47 +08:00
_widgetBuilder = (context) => Editor(
jsonString: _jsonString,
onEditorStateChange: (editorState) {
_editorState = editorState;
},
textDirection: textDirection,
);
},
);
2023-01-07 09:38:38 +08:00
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}
2022-11-09 11:50:08 +08:00
void _exportFile(
EditorState editorState,
ExportFileType fileType,
) async {
var result = '';
switch (fileType) {
2023-07-03 19:19:32 +08:00
case ExportFileType.documentJson:
result = jsonEncode(editorState.document.toJson());
break;
case ExportFileType.markdown:
result = documentToMarkdown(editorState.document);
break;
case ExportFileType.pdf:
result = documentToMarkdown(editorState.document);
break;
case ExportFileType.delta:
throw UnimplementedError();
}
2022-11-09 11:50:08 +08:00
if (kIsWeb) {
final blob = html.Blob([result], 'text/plain', 'native');
html.AnchorElement(
href: html.Url.createObjectUrlFromBlob(blob).toString(),
)
..setAttribute('download', 'document.${fileType.extension}')
..click();
} else if (UniversalPlatform.isMobile) {
final appStorageDirectory = await getApplicationDocumentsDirectory();
final path = File(
'${appStorageDirectory.path}/${DateTime.now()}.${fileType.extension}',
);
await path.writeAsString(result);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'This document is saved to the ${appStorageDirectory.path}',
),
),
);
}
} else {
// for desktop
2026-01-29 14:12:57 +08:00
final path = await FilePicker.platform.saveFile(
fileName: 'document.${fileType.extension}',
2022-11-09 11:50:08 +08:00
);
if (path != null) {
await File(path).writeAsString(result);
if (fileType == ExportFileType.pdf) {
final pdf = await PdfHTMLEncoder(
fontFallback: [
await PdfGoogleFonts.notoColorEmoji(),
await PdfGoogleFonts.notoColorEmojiRegular(),
],
).convert(result);
await File(path).writeAsBytes(await pdf.save());
}
2022-11-09 11:50:08 +08:00
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('This document is saved to the $path'),
2022-11-09 11:50:08 +08:00
),
);
}
}
}
}
void _importFile(ExportFileType fileType) async {
2026-01-29 14:12:57 +08:00
final result = await FilePicker.platform.pickFiles(
allowMultiple: false,
allowedExtensions: [fileType.extension],
type: FileType.custom,
);
2022-11-09 15:59:00 +08:00
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);
}
2022-11-09 15:59:00 +08:00
var jsonString = '';
switch (fileType) {
2023-07-03 19:19:32 +08:00
case ExportFileType.documentJson:
2023-03-09 14:38:54 +07:00
jsonString = plainText;
break;
case ExportFileType.markdown:
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
break;
case ExportFileType.delta:
2023-07-03 19:19:32 +08:00
final delta = Delta.fromJson(jsonDecode(plainText));
final document = quillDeltaEncoder.convert(delta);
jsonString = jsonEncode(document.toJson());
break;
case ExportFileType.pdf:
throw UnimplementedError();
}
if (mounted) {
_loadEditor(context, Future<String>.value(jsonString));
}
}
}
2023-09-11 15:03:14 +08:00
String generateRandomString(int len) {
var r = Random();
return String.fromCharCodes(
List.generate(len, (index) => r.nextInt(33) + 89),
);
}