Forráskód Böngészése

feat: implement theme customizer showcase

Lucas.Xu 2 éve
szülő
commit
e20ce9052a

+ 79 - 7
frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart

@@ -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,
+    ]);
+  }
 }

+ 0 - 206
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -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;
-      });
-    }
-  }
 }

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart

@@ -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 {

+ 4 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart

@@ -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)) {

+ 7 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart

@@ -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,

+ 2 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart

@@ -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,