feat: refactor theme plugin, use themedata extension

This commit is contained in:
Lucas.Xu 2022-10-25 20:01:17 +08:00
parent 68da3955c1
commit cdee706f46
9 changed files with 550 additions and 74 deletions

View File

@ -38,6 +38,7 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
// extensions: [HeadingPluginStyle.light],
),
home: const MyHomePage(title: 'AppFlowyEditor Example'),
);
@ -125,28 +126,48 @@ class _MyHomePageState extends State<MyHomePage> {
_editorState!.transactionStream.listen((event) {
debugPrint('Transaction: ${event.toJson()}');
});
return Container(
color: darkMode ? Colors.black : Colors.white,
width: MediaQuery.of(context).size.width,
child: AppFlowyEditor(
editorState: _editorState!,
editorStyle: _editorStyle,
editable: true,
customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(),
'tex': TeXBlockNodeWidgetBuidler(),
'horizontal_rule': HorizontalRuleWidgetBuilder(),
},
shortcutEvents: [
enterInCodeBlock,
ignoreKeysInCodeBlock,
insertHorizontalRule,
],
selectionMenuItems: [
codeBlockMenuItem,
teXBlockMenuItem,
horizontalRuleMenuItem,
],
final themeData = darkMode
? ThemeData.dark().copyWith(extensions: [
HeadingPluginStyle.dark,
CheckboxPluginStyle.dark,
NumberListPluginStyle.dark,
QuotedTextPluginStyle.dark,
BulletedListPluginStyle.dark
])
: ThemeData.light().copyWith(
extensions: [
HeadingPluginStyle.light,
CheckboxPluginStyle.light,
NumberListPluginStyle.light,
QuotedTextPluginStyle.light,
BulletedListPluginStyle.light
],
);
return Theme(
data: themeData,
child: Container(
color: darkMode ? Colors.black : Colors.white,
width: MediaQuery.of(context).size.width,
child: AppFlowyEditor(
editorState: _editorState!,
editorStyle: _editorStyle,
editable: true,
customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(),
'tex': TeXBlockNodeWidgetBuidler(),
'horizontal_rule': HorizontalRuleWidgetBuilder(),
},
shortcutEvents: [
enterInCodeBlock,
ignoreKeysInCodeBlock,
insertHorizontalRule,
],
selectionMenuItems: [
codeBlockMenuItem,
teXBlockMenuItem,
horizontalRuleMenuItem,
],
),
),
);
} else {

View File

@ -31,3 +31,4 @@ export 'src/render/rich_text/default_selectable.dart';
export 'src/render/rich_text/flowy_rich_text.dart';
export 'src/render/selection_menu/selection_menu_widget.dart';
export 'src/l10n/l10n.dart';
export 'src/render/style/built_in_plugin_styles.dart';

View File

@ -1,10 +1,10 @@
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/built_in_plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
@ -45,11 +45,7 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
// customize
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
with
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
BuiltInTextWidgetMixin {
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
final iconKey = GlobalKey();
@ -64,17 +60,23 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
Color get bulletColor {
final bulletColor = widget.editorState.editorStyle.style(
widget.editorState,
widget.textNode,
'bulletColor',
);
if (bulletColor is Color) {
return bulletColor;
}
return Colors.black;
}
BulletedListPluginStyle get style =>
Theme.of(context).extension<BulletedListPluginStyle>()!;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget buildWithSingle(BuildContext context) {
@ -83,13 +85,9 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
Container(
key: iconKey,
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
color: bulletColor,
name: 'point',
child: icon,
),
Flexible(
child: FlowyRichText(

View File

@ -1,6 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/commands/text/text_commands.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
@ -39,11 +38,7 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
}
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
with
SelectableMixin,
DefaultSelectable,
BuiltInStyleMixin,
BuiltInTextWidgetMixin {
with SelectableMixin, DefaultSelectable, BuiltInTextWidgetMixin {
@override
final iconKey = GlobalKey();
@ -58,6 +53,24 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
CheckboxPluginStyle get style =>
Theme.of(context).extension<CheckboxPluginStyle>()!;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget buildWithSingle(BuildContext context) {
final check = widget.textNode.attributes.check;
@ -68,12 +81,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
children: [
GestureDetector(
key: iconKey,
child: FlowySvg(
width: iconSize?.width,
height: iconSize?.height,
padding: iconPadding,
name: check ? 'check' : 'uncheck',
),
child: icon,
onTap: () async {
await widget.editorState.formatTextToCheckbox(
widget.editorState,

View File

@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/built_in_plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
@ -43,7 +44,7 @@ class HeadingTextNodeWidget extends BuiltInTextWidget {
// customize
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
with SelectableMixin, DefaultSelectable {
@override
GlobalKey? get iconKey => null;
@ -58,6 +59,19 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
return padding.topLeft;
}
HeadingPluginStyle get style =>
Theme.of(context).extension<HeadingPluginStyle>()!;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
@override
Widget build(BuildContext context) {
return Padding(

View File

@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/plugin_style.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
@ -43,7 +44,7 @@ class NumberListTextNodeWidget extends BuiltInTextWidget {
}
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
with SelectableMixin, DefaultSelectable {
@override
final iconKey = GlobalKey();
@ -70,6 +71,24 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
return Colors.black;
}
NumberListPluginStyle get style =>
Theme.of(context).extension<NumberListPluginStyle>()!;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget build(BuildContext context) {
return Padding(
@ -79,15 +98,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
children: [
Container(
key: iconKey,
padding: iconPadding,
child: Text(
'${widget.textNode.attributes.number.toString()}.',
style: TextStyle(
fontSize: widget.editorState.editorStyle.textStyle
.defaultTextStyle.fontSize,
color: numberColor,
),
),
child: icon,
),
Flexible(
child: FlowyRichText(

View File

@ -1,10 +1,10 @@
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:appflowy_editor/src/render/style/built_in_plugin_styles.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
@ -44,7 +44,7 @@ class QuotedTextNodeWidget extends BuiltInTextWidget {
// customize
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
with SelectableMixin, DefaultSelectable {
@override
final iconKey = GlobalKey();
@ -59,6 +59,24 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
return super.baseOffset.translate(0, padding.top);
}
QuotedTextPluginStyle get style =>
Theme.of(context).extension<QuotedTextPluginStyle>()!;
EdgeInsets get padding => style.padding(
widget.editorState,
widget.textNode,
);
TextStyle get textStyle => style.textStyle(
widget.editorState,
widget.textNode,
);
Widget get icon => style.icon(
widget.editorState,
widget.textNode,
);
@override
Widget build(BuildContext context) {
return Padding(
@ -67,11 +85,9 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FlowySvg(
Container(
key: iconKey,
width: iconSize?.width,
padding: iconPadding,
name: 'quote',
child: icon,
),
Flexible(
child: FlowyRichText(

View File

@ -4,6 +4,100 @@ import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
class EditorStyleV2 extends ThemeExtension<EditorStyleV2> {
// Editor styles
final EdgeInsets? padding;
final Color? cursorColor;
final Color? selectionColor;
// Text styles
final TextStyle? textStyle;
final TextStyle? placeholderTextStyle;
final double lineHeight;
// Rich text styles
final TextStyle? bold;
final TextStyle? italic;
final TextStyle? underline;
final TextStyle? strikethrough;
final TextStyle? href;
final TextStyle? code;
final String? highlightColorHex;
EditorStyleV2({
required this.padding,
required this.cursorColor,
required this.selectionColor,
required this.textStyle,
required this.placeholderTextStyle,
required this.bold,
required this.italic,
required this.underline,
required this.strikethrough,
required this.href,
required this.code,
required this.highlightColorHex,
required this.lineHeight,
});
@override
EditorStyleV2 copyWith({
EdgeInsets? padding,
Color? cursorColor,
Color? selectionColor,
TextStyle? textStyle,
TextStyle? placeholderTextStyle,
TextStyle? bold,
TextStyle? italic,
TextStyle? underline,
TextStyle? strikethrough,
TextStyle? href,
TextStyle? code,
String? highlightColorHex,
double? lineHeight,
}) {
return EditorStyleV2(
padding: padding ?? this.padding,
cursorColor: cursorColor ?? this.cursorColor,
selectionColor: selectionColor ?? this.selectionColor,
textStyle: textStyle ?? this.textStyle,
placeholderTextStyle: placeholderTextStyle ?? this.placeholderTextStyle,
bold: bold ?? this.bold,
italic: italic ?? this.italic,
underline: underline ?? this.underline,
strikethrough: strikethrough ?? this.strikethrough,
href: href ?? this.href,
code: code ?? this.code,
highlightColorHex: highlightColorHex ?? this.highlightColorHex,
lineHeight: lineHeight ?? this.lineHeight,
);
}
@override
ThemeExtension<EditorStyleV2> lerp(
ThemeExtension<EditorStyleV2>? other, double t) {
if (other == null || other is! EditorStyleV2) {
return this;
}
return EditorStyleV2(
padding: EdgeInsets.lerp(padding, other.padding, t),
cursorColor: Color.lerp(cursorColor, other.cursorColor, t),
selectionColor: Color.lerp(selectionColor, other.selectionColor, t),
textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
placeholderTextStyle:
TextStyle.lerp(placeholderTextStyle, other.placeholderTextStyle, t),
bold: TextStyle.lerp(bold, other.bold, t),
italic: TextStyle.lerp(italic, other.italic, t),
underline: TextStyle.lerp(underline, other.underline, t),
strikethrough: TextStyle.lerp(strikethrough, other.strikethrough, t),
href: TextStyle.lerp(href, other.href, t),
code: TextStyle.lerp(code, other.code, t),
highlightColorHex: highlightColorHex,
lineHeight: lineHeight,
);
}
}
typedef PluginStyler = Object Function(EditorState editorState, Node node);
typedef PluginStyle = Map<String, PluginStyler>;
@ -41,6 +135,7 @@ class EditorStyle {
return null;
}
@override
EditorStyle copyWith({
EdgeInsets? padding,
BuiltInTextStyle? textStyle,

View File

@ -0,0 +1,312 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:flutter/material.dart';
typedef TextStyleCustomizer = TextStyle Function(
EditorState editorState, TextNode textNode);
typedef PaddingCustomizer = EdgeInsets Function(
EditorState editorState, TextNode textNode);
typedef IconCustomizer = Widget Function(
EditorState editorState, TextNode textNode);
class HeadingPluginStyle extends ThemeExtension<HeadingPluginStyle> {
const HeadingPluginStyle({
required this.textStyle,
required this.padding,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
@override
HeadingPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
}) {
return HeadingPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
);
}
@override
ThemeExtension<HeadingPluginStyle> lerp(
ThemeExtension<HeadingPluginStyle>? other, double t) {
if (other is! HeadingPluginStyle) {
return this;
}
return HeadingPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
);
}
static final light = HeadingPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (editorState, textNode) {
final headingToFontSize = {
'h1': 32.0,
'h2': 28.0,
'h3': 24.0,
'h4': 18.0,
'h5': 18.0,
'h6': 18.0,
};
final fontSize = headingToFontSize[textNode.attributes.heading] ?? 18.0;
return TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
);
},
);
static final dark = light;
}
class CheckboxPluginStyle extends ThemeExtension<CheckboxPluginStyle> {
const CheckboxPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
CheckboxPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return CheckboxPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<CheckboxPluginStyle> lerp(
ThemeExtension<CheckboxPluginStyle>? other, double t) {
if (other is! CheckboxPluginStyle) {
return this;
}
return CheckboxPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = CheckboxPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (editorState, textNode) => const TextStyle(),
icon: (editorState, textNode) {
final isCheck = textNode.attributes.check;
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
height: iconSize.height,
padding: iconPadding,
name: isCheck ? 'check' : 'uncheck',
);
},
);
static final dark = light;
}
class BulletedListPluginStyle extends ThemeExtension<BulletedListPluginStyle> {
const BulletedListPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
BulletedListPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return BulletedListPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<BulletedListPluginStyle> lerp(
ThemeExtension<BulletedListPluginStyle>? other, double t) {
if (other is! BulletedListPluginStyle) {
return this;
}
return BulletedListPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = BulletedListPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (_, __) => const TextStyle(),
icon: (_, __) {
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
height: iconSize.height,
padding: iconPadding,
color: Colors.black,
name: 'point',
);
},
);
static final dark = light.copyWith(icon: (_, __) {
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
height: iconSize.height,
padding: iconPadding,
color: Colors.white,
name: 'point',
);
});
}
class NumberListPluginStyle extends ThemeExtension<NumberListPluginStyle> {
const NumberListPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
NumberListPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return NumberListPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<NumberListPluginStyle> lerp(
ThemeExtension<NumberListPluginStyle>? other,
double t,
) {
if (other is! NumberListPluginStyle) {
return this;
}
return NumberListPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = NumberListPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (_, __) => const TextStyle(),
icon: (_, textNode) {
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
return Container(
padding: iconPadding,
child: Text(
'${textNode.attributes.number.toString()}.',
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
);
},
);
static final dark = light.copyWith(icon: (editorState, textNode) {
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
return Container(
padding: iconPadding,
child: Text(
'${textNode.attributes.number.toString()}.',
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
);
});
}
class QuotedTextPluginStyle extends ThemeExtension<QuotedTextPluginStyle> {
const QuotedTextPluginStyle({
required this.textStyle,
required this.padding,
required this.icon,
});
final TextStyleCustomizer textStyle;
final PaddingCustomizer padding;
final IconCustomizer icon;
@override
QuotedTextPluginStyle copyWith({
TextStyleCustomizer? textStyle,
PaddingCustomizer? padding,
IconCustomizer? icon,
}) {
return QuotedTextPluginStyle(
textStyle: textStyle ?? this.textStyle,
padding: padding ?? this.padding,
icon: icon ?? this.icon,
);
}
@override
ThemeExtension<QuotedTextPluginStyle> lerp(
ThemeExtension<QuotedTextPluginStyle>? other, double t) {
if (other is! QuotedTextPluginStyle) {
return this;
}
return QuotedTextPluginStyle(
textStyle: other.textStyle,
padding: other.padding,
icon: other.icon,
);
}
static final light = QuotedTextPluginStyle(
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
textStyle: (_, __) => const TextStyle(),
icon: (_, __) {
const iconSize = Size.square(20.0);
const iconPadding = EdgeInsets.only(right: 5.0);
return FlowySvg(
width: iconSize.width,
padding: iconPadding,
name: 'quote',
);
},
);
static final dark = light;
}