feat: implement theme customizer showcase

This commit is contained in:
Lucas.Xu 2022-11-09 15:36:30 +08:00
parent 853be71bf5
commit e20ce9052a
6 changed files with 95 additions and 214 deletions

View File

@ -7,6 +7,7 @@ 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 {
@ -39,15 +40,28 @@ 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();
_widgetBuilder = (context) {
_editorState = EditorState.empty();
return AppFlowyEditor(editorState: EditorState.empty());
};
_jsonString = Future<String>.value(
jsonEncode(EditorState.empty().document.toJson()),
);
_widgetBuilder = (context) => SimpleEditor(
jsonString: _jsonString,
themeData: _themeData,
onEditorStateChange: (editorState) {
_editorState = editorState;
},
);
}
@override
@ -108,8 +122,27 @@ class _HomePageState extends State<HomePage> {
// Theme Demo
_buildSeparator(context, 'Theme Demo'),
_buildListTile(context, 'Bulit In Dark Mode', () {}),
_buildListTile(context, 'Custom Theme', () {}),
_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);
});
}),
],
),
);
@ -165,10 +198,12 @@ class _HomePageState extends State<HomePage> {
}
void _loadEditor(BuildContext context, Future<String> jsonString) {
_jsonString = jsonString;
setState(
() {
_widgetBuilder = (context) => SimpleEditor(
jsonString: jsonString,
jsonString: _jsonString,
themeData: _themeData,
onEditorStateChange: (editorState) {
_editorState = editorState;
},
@ -245,4 +280,41 @@ class _HomePageState extends State<HomePage> {
_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,
]);
}
}

View File

@ -1,24 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:example/home_page.dart';
import 'package:example/plugin/editor_theme.dart';
import 'package:flutter/foundation.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:path_provider/path_provider.dart';
import 'package:universal_html/html.dart' as html;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'expandable_floating_action_button.dart';
void main() {
runApp(const MyApp());
}
@ -51,200 +37,8 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
int _pageIndex = 0;
EditorState? _editorState;
bool darkMode = false;
Future<String>? _jsonString;
ThemeData? _editorThemeData;
@override
Widget build(BuildContext context) {
return const HomePage();
}
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(BuildContext context) {
return FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();
});
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;
});
}
}
}

View File

@ -7,10 +7,12 @@ 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
@ -30,6 +32,7 @@ class SimpleEditor extends StatelessWidget {
onEditorStateChange(editorState);
return AppFlowyEditor(
editorState: editorState,
themeData: themeData,
autoFocus: editorState.document.isEmpty,
);
} else {

View File

@ -115,6 +115,10 @@ class Document {
return true;
}
if (root.children.length > 1) {
return false;
}
final node = root.children.first;
if (node is TextNode &&
(node.delta.isEmpty || node.delta.toPlainText().isEmpty)) {

View File

@ -11,6 +11,7 @@ Iterable<ThemeExtension<dynamic>> get darkEditorStyleExtension => [
class EditorStyle extends ThemeExtension<EditorStyle> {
// Editor styles
final EdgeInsets? padding;
final Color? backgroundColor;
final Color? cursorColor;
final Color? selectionColor;
@ -39,6 +40,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
EditorStyle({
required this.padding,
required this.backgroundColor,
required this.cursorColor,
required this.selectionColor,
required this.selectionMenuBackgroundColor,
@ -63,6 +65,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
@override
EditorStyle copyWith({
EdgeInsets? padding,
Color? backgroundColor,
Color? cursorColor,
Color? selectionColor,
Color? selectionMenuBackgroundColor,
@ -84,6 +87,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
}) {
return EditorStyle(
padding: padding ?? this.padding,
backgroundColor: backgroundColor ?? this.backgroundColor,
cursorColor: cursorColor ?? this.cursorColor,
selectionColor: selectionColor ?? this.selectionColor,
selectionMenuBackgroundColor:
@ -120,6 +124,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
}
return EditorStyle(
padding: EdgeInsets.lerp(padding, other.padding, t),
backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t),
cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
textPadding: EdgeInsets.lerp(textPadding, other.textPadding, t),
selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
@ -155,6 +160,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
static final light = EditorStyle(
padding: const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
backgroundColor: Colors.white,
cursorColor: const Color(0xFF00BCF0),
selectionColor: const Color.fromARGB(53, 111, 201, 231),
selectionMenuBackgroundColor: const Color(0xFFFFFFFF),
@ -184,6 +190,7 @@ class EditorStyle extends ThemeExtension<EditorStyle> {
);
static final dark = light.copyWith(
backgroundColor: Colors.black,
textStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
placeholderTextStyle: TextStyle(
fontSize: 16.0,

View File

@ -119,7 +119,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
data: widget.themeData,
child: AppFlowyScroll(
key: editorState.service.scrollServiceKey,
child: Padding(
child: Container(
color: editorStyle.backgroundColor,
padding: editorStyle.padding!,
child: AppFlowySelection(
key: editorState.service.selectionServiceKey,