Skip to content

Commit e20ce90

Browse files
committed
feat: implement theme customizer showcase
1 parent 853be71 commit e20ce90

File tree

6 files changed

+95
-214
lines changed

6 files changed

+95
-214
lines changed

frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
77
import 'package:flutter/foundation.dart';
88
import 'package:flutter/material.dart';
99
import 'package:flutter/services.dart';
10+
import 'package:google_fonts/google_fonts.dart';
1011
import 'package:universal_html/html.dart' as html;
1112

1213
enum ExportFileType {
@@ -39,15 +40,28 @@ class _HomePageState extends State<HomePage> {
3940
final _scaffoldKey = GlobalKey<ScaffoldState>();
4041
late WidgetBuilder _widgetBuilder;
4142
late EditorState _editorState;
43+
late Future<String> _jsonString;
44+
ThemeData _themeData = ThemeData.light().copyWith(
45+
extensions: [
46+
...lightEditorStyleExtension,
47+
...lightPlguinStyleExtension,
48+
],
49+
);
4250

4351
@override
4452
void initState() {
4553
super.initState();
4654

47-
_widgetBuilder = (context) {
48-
_editorState = EditorState.empty();
49-
return AppFlowyEditor(editorState: EditorState.empty());
50-
};
55+
_jsonString = Future<String>.value(
56+
jsonEncode(EditorState.empty().document.toJson()),
57+
);
58+
_widgetBuilder = (context) => SimpleEditor(
59+
jsonString: _jsonString,
60+
themeData: _themeData,
61+
onEditorStateChange: (editorState) {
62+
_editorState = editorState;
63+
},
64+
);
5165
}
5266

5367
@override
@@ -108,8 +122,27 @@ class _HomePageState extends State<HomePage> {
108122

109123
// Theme Demo
110124
_buildSeparator(context, 'Theme Demo'),
111-
_buildListTile(context, 'Bulit In Dark Mode', () {}),
112-
_buildListTile(context, 'Custom Theme', () {}),
125+
_buildListTile(context, 'Bulit In Dark Mode', () {
126+
_jsonString = Future<String>.value(
127+
jsonEncode(_editorState.document.toJson()).toString(),
128+
);
129+
setState(() {
130+
_themeData = ThemeData.dark().copyWith(
131+
extensions: [
132+
...darkEditorStyleExtension,
133+
...darkPlguinStyleExtension,
134+
],
135+
);
136+
});
137+
}),
138+
_buildListTile(context, 'Custom Theme', () {
139+
_jsonString = Future<String>.value(
140+
jsonEncode(_editorState.document.toJson()).toString(),
141+
);
142+
setState(() {
143+
_themeData = _customizeEditorTheme(context);
144+
});
145+
}),
113146
],
114147
),
115148
);
@@ -165,10 +198,12 @@ class _HomePageState extends State<HomePage> {
165198
}
166199

167200
void _loadEditor(BuildContext context, Future<String> jsonString) {
201+
_jsonString = jsonString;
168202
setState(
169203
() {
170204
_widgetBuilder = (context) => SimpleEditor(
171-
jsonString: jsonString,
205+
jsonString: _jsonString,
206+
themeData: _themeData,
172207
onEditorStateChange: (editorState) {
173208
_editorState = editorState;
174209
},
@@ -245,4 +280,41 @@ class _HomePageState extends State<HomePage> {
245280
_loadEditor(context, Future<String>.value(jsonString));
246281
}
247282
}
283+
284+
ThemeData _customizeEditorTheme(BuildContext context) {
285+
final dark = EditorStyle.dark;
286+
final editorStyle = dark.copyWith(
287+
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 150),
288+
cursorColor: Colors.blue.shade600,
289+
selectionColor: Colors.yellow.shade600.withOpacity(0.5),
290+
textStyle: GoogleFonts.poppins().copyWith(
291+
fontSize: 14,
292+
color: Colors.grey,
293+
),
294+
placeholderTextStyle: GoogleFonts.poppins().copyWith(
295+
fontSize: 14,
296+
color: Colors.grey.shade500,
297+
),
298+
code: dark.code?.copyWith(
299+
backgroundColor: Colors.lightBlue.shade200,
300+
fontStyle: FontStyle.italic,
301+
),
302+
highlightColorHex: '0x60FF0000', // red
303+
);
304+
305+
final quote = QuotedTextPluginStyle.dark.copyWith(
306+
textStyle: (_, __) => GoogleFonts.poppins().copyWith(
307+
fontSize: 14,
308+
color: Colors.blue.shade400,
309+
fontStyle: FontStyle.italic,
310+
fontWeight: FontWeight.w700,
311+
),
312+
);
313+
314+
return Theme.of(context).copyWith(extensions: [
315+
editorStyle,
316+
...darkPlguinStyleExtension,
317+
quote,
318+
]);
319+
}
248320
}
Lines changed: 0 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,10 @@
1-
import 'dart:convert';
2-
import 'dart:io';
3-
41
import 'package:example/home_page.dart';
5-
import 'package:example/plugin/editor_theme.dart';
6-
import 'package:flutter/foundation.dart';
72
import 'package:flutter/material.dart';
8-
import 'package:flutter/services.dart';
93

10-
import 'package:example/plugin/code_block_node_widget.dart';
11-
import 'package:example/plugin/horizontal_rule_node_widget.dart';
12-
import 'package:example/plugin/tex_block_node_widget.dart';
13-
import 'package:file_picker/file_picker.dart';
144
import 'package:flutter_localizations/flutter_localizations.dart';
15-
import 'package:path_provider/path_provider.dart';
16-
import 'package:universal_html/html.dart' as html;
175

186
import 'package:appflowy_editor/appflowy_editor.dart';
197

20-
import 'expandable_floating_action_button.dart';
21-
228
void main() {
239
runApp(const MyApp());
2410
}
@@ -51,200 +37,8 @@ class MyHomePage extends StatefulWidget {
5137
}
5238

5339
class _MyHomePageState extends State<MyHomePage> {
54-
int _pageIndex = 0;
55-
EditorState? _editorState;
56-
bool darkMode = false;
57-
Future<String>? _jsonString;
58-
59-
ThemeData? _editorThemeData;
60-
6140
@override
6241
Widget build(BuildContext context) {
6342
return const HomePage();
6443
}
65-
66-
Widget _buildEditor(BuildContext context) {
67-
if (_jsonString != null) {
68-
return _buildEditorWithJsonString(_jsonString!);
69-
}
70-
if (_pageIndex == 0) {
71-
return _buildEditorWithJsonString(
72-
rootBundle.loadString('assets/example.json'),
73-
);
74-
} else if (_pageIndex == 1) {
75-
return _buildEditorWithJsonString(
76-
Future.value(
77-
jsonEncode(EditorState.empty().document.toJson()),
78-
),
79-
);
80-
}
81-
throw UnimplementedError();
82-
}
83-
84-
Widget _buildEditorWithJsonString(Future<String> jsonString) {
85-
return FutureBuilder<String>(
86-
future: jsonString,
87-
builder: (_, snapshot) {
88-
if (snapshot.hasData &&
89-
snapshot.connectionState == ConnectionState.done) {
90-
_editorState ??= EditorState(
91-
document: Document.fromJson(
92-
Map<String, Object>.from(
93-
json.decode(snapshot.data!),
94-
),
95-
),
96-
);
97-
_editorState!.logConfiguration
98-
..level = LogLevel.all
99-
..handler = (message) {
100-
debugPrint(message);
101-
};
102-
_editorState!.transactionStream.listen((event) {
103-
debugPrint('Transaction: ${event.toJson()}');
104-
});
105-
_editorThemeData ??= Theme.of(context).copyWith(extensions: [
106-
if (darkMode) ...darkEditorStyleExtension,
107-
if (darkMode) ...darkPlguinStyleExtension,
108-
if (!darkMode) ...lightEditorStyleExtension,
109-
if (!darkMode) ...lightPlguinStyleExtension,
110-
]);
111-
return Container(
112-
color: darkMode ? Colors.black : Colors.white,
113-
width: MediaQuery.of(context).size.width,
114-
child: AppFlowyEditor(
115-
editorState: _editorState!,
116-
editable: true,
117-
autoFocus: _editorState!.document.isEmpty,
118-
themeData: _editorThemeData,
119-
customBuilders: {
120-
'text/code_block': CodeBlockNodeWidgetBuilder(),
121-
'tex': TeXBlockNodeWidgetBuidler(),
122-
'horizontal_rule': HorizontalRuleWidgetBuilder(),
123-
},
124-
shortcutEvents: [
125-
enterInCodeBlock,
126-
ignoreKeysInCodeBlock,
127-
insertHorizontalRule,
128-
],
129-
selectionMenuItems: [
130-
codeBlockMenuItem,
131-
teXBlockMenuItem,
132-
horizontalRuleMenuItem,
133-
],
134-
),
135-
);
136-
} else {
137-
return const Center(
138-
child: CircularProgressIndicator(),
139-
);
140-
}
141-
},
142-
);
143-
}
144-
145-
Widget _buildExpandableFab(BuildContext context) {
146-
return FloatingActionButton(onPressed: () {
147-
Scaffold.of(context).openDrawer();
148-
});
149-
return ExpandableFab(
150-
distance: 112.0,
151-
children: [
152-
ActionButton(
153-
icon: const Icon(Icons.abc),
154-
onPressed: () => _switchToPage(0),
155-
),
156-
ActionButton(
157-
icon: const Icon(Icons.abc),
158-
onPressed: () => _switchToPage(1),
159-
),
160-
ActionButton(
161-
icon: const Icon(Icons.print),
162-
onPressed: () => _exportDocument(_editorState!),
163-
),
164-
ActionButton(
165-
icon: const Icon(Icons.import_export),
166-
onPressed: () async => await _importDocument(),
167-
),
168-
ActionButton(
169-
icon: const Icon(Icons.dark_mode),
170-
onPressed: () {
171-
setState(() {
172-
darkMode = !darkMode;
173-
});
174-
},
175-
),
176-
ActionButton(
177-
icon: const Icon(Icons.color_lens),
178-
onPressed: () {
179-
setState(() {
180-
_editorThemeData = customizeEditorTheme(context);
181-
darkMode = true;
182-
});
183-
},
184-
),
185-
],
186-
);
187-
}
188-
189-
void _exportDocument(EditorState editorState) async {
190-
final document = editorState.document.toJson();
191-
final json = jsonEncode(document);
192-
if (kIsWeb) {
193-
final blob = html.Blob([json], 'text/plain', 'native');
194-
html.AnchorElement(
195-
href: html.Url.createObjectUrlFromBlob(blob).toString(),
196-
)
197-
..setAttribute('download', 'editor.json')
198-
..click();
199-
} else {
200-
final directory = await getTemporaryDirectory();
201-
final path = directory.path;
202-
final file = File('$path/editor.json');
203-
await file.writeAsString(json);
204-
205-
if (mounted) {
206-
ScaffoldMessenger.of(context).showSnackBar(
207-
SnackBar(
208-
content: Text('The document is saved to the ${file.path}'),
209-
),
210-
);
211-
}
212-
}
213-
}
214-
215-
Future<void> _importDocument() async {
216-
if (kIsWeb) {
217-
final result = await FilePicker.platform.pickFiles(
218-
allowMultiple: false,
219-
allowedExtensions: ['json'],
220-
type: FileType.custom,
221-
);
222-
final bytes = result?.files.first.bytes;
223-
if (bytes != null) {
224-
final jsonString = const Utf8Decoder().convert(bytes);
225-
setState(() {
226-
_editorState = null;
227-
_jsonString = Future.value(jsonString);
228-
});
229-
}
230-
} else {
231-
final directory = await getTemporaryDirectory();
232-
final path = '${directory.path}/editor.json';
233-
final file = File(path);
234-
setState(() {
235-
_editorState = null;
236-
_jsonString = file.readAsString();
237-
});
238-
}
239-
}
240-
241-
void _switchToPage(int pageIndex) {
242-
if (pageIndex != _pageIndex) {
243-
setState(() {
244-
_editorThemeData = null;
245-
_editorState = null;
246-
_pageIndex = pageIndex;
247-
});
248-
}
249-
}
25044
}

frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ class SimpleEditor extends StatelessWidget {
77
const SimpleEditor({
88
super.key,
99
required this.jsonString,
10+
required this.themeData,
1011
required this.onEditorStateChange,
1112
});
1213

1314
final Future<String> jsonString;
15+
final ThemeData themeData;
1416
final void Function(EditorState editorState) onEditorStateChange;
1517

1618
@override
@@ -30,6 +32,7 @@ class SimpleEditor extends StatelessWidget {
3032
onEditorStateChange(editorState);
3133
return AppFlowyEditor(
3234
editorState: editorState,
35+
themeData: themeData,
3336
autoFocus: editorState.document.isEmpty,
3437
);
3538
} else {

frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/document.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ class Document {
115115
return true;
116116
}
117117

118+
if (root.children.length > 1) {
119+
return false;
120+
}
121+
118122
final node = root.children.first;
119123
if (node is TextNode &&
120124
(node.delta.isEmpty || node.delta.toPlainText().isEmpty)) {

0 commit comments

Comments
 (0)