* feat: adjust toggle list block * chore: upgrade Flutter to master channel * chore: upgrade pbxproj * fix: toolbar copy button not closing on mobile editor * fix: formatting issues and add missing commas * chore: add stable branch to GitHub Actions workflow * chore: add missing trailing commas * chore: adjust indentation padding in block component configuration and unit tests
393 lines
11 KiB
Dart
393 lines
11 KiB
Dart
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:example/pages/editor_list.dart';
|
|
import 'package:example/pages/focus_example_for_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:path_provider/path_provider.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({super.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;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_jsonString = PlatformExtension.isDesktopOrWeb
|
|
? rootBundle.loadString('assets/example.json')
|
|
: rootBundle.loadString('assets/mobile_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 = PlatformExtension.isDesktopOrWeb
|
|
? rootBundle.loadString('assets/example.json')
|
|
: rootBundle.loadString('assets/mobile_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<String>.value(
|
|
jsonEncode(
|
|
html.toJson(),
|
|
).toString(),
|
|
);
|
|
if (mounted) {
|
|
_loadEditor(context, jsonString);
|
|
}
|
|
}),
|
|
_buildListTile(context, 'With Empty Document', () {
|
|
final jsonString = Future<String>.value(
|
|
jsonEncode(
|
|
EditorState.blank(withInitialText: true).document.toJson(),
|
|
).toString(),
|
|
);
|
|
_loadEditor(context, jsonString);
|
|
}),
|
|
|
|
// Theme Demo
|
|
_buildSeparator(context, 'Showcases'),
|
|
_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(),
|
|
),
|
|
);
|
|
}),
|
|
|
|
// 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<void> _loadEditor(
|
|
BuildContext context,
|
|
Future<String> jsonString, {
|
|
TextDirection textDirection = TextDirection.ltr,
|
|
}) async {
|
|
final completer = Completer<void>();
|
|
_jsonString = jsonString;
|
|
setState(
|
|
() {
|
|
_widgetBuilder = (context) => Editor(
|
|
jsonString: _jsonString,
|
|
onEditorStateChange: (editorState) {
|
|
_editorState = editorState;
|
|
},
|
|
textDirection: textDirection,
|
|
);
|
|
},
|
|
);
|
|
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 blob = html.Blob([result], 'text/plain', 'native');
|
|
html.AnchorElement(
|
|
href: html.Url.createObjectUrlFromBlob(blob).toString(),
|
|
)
|
|
..setAttribute('download', 'document.${fileType.extension}')
|
|
..click();
|
|
} else if (PlatformExtension.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
|
|
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'),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<String>.value(jsonString));
|
|
}
|
|
}
|
|
}
|
|
|
|
String generateRandomString(int len) {
|
|
var r = Random();
|
|
return String.fromCharCodes(
|
|
List.generate(len, (index) => r.nextInt(33) + 89),
|
|
);
|
|
}
|