feat: support block wrapper (#1101)
This commit is contained in:
parent
e56746ac65
commit
470c4e77c7
6 changed files with 326 additions and 12 deletions
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:example/pages/animated_markdown_page.dart';
|
||||||
import 'package:example/pages/auto_complete_editor.dart';
|
import 'package:example/pages/auto_complete_editor.dart';
|
||||||
import 'package:example/pages/auto_expand_editor.dart';
|
import 'package:example/pages/auto_expand_editor.dart';
|
||||||
import 'package:example/pages/collab_editor.dart';
|
import 'package:example/pages/collab_editor.dart';
|
||||||
|
|
@ -185,6 +186,14 @@ class _HomePageState extends State<HomePage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
_buildListTile(context, 'Animated Markdown Editor', () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const AnimatedMarkdownPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
_buildListTile(context, 'Auto complete Editor', () {
|
_buildListTile(context, 'Auto complete Editor', () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
270
example/lib/pages/animated_markdown_page.dart
Normal file
270
example/lib/pages/animated_markdown_page.dart
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AnimatedMarkdownPage extends StatefulWidget {
|
||||||
|
const AnimatedMarkdownPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AnimatedMarkdownPage> createState() => _AnimatedMarkdownPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedMarkdownPageState extends State<AnimatedMarkdownPage>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
int offset = 0;
|
||||||
|
String markdown = '';
|
||||||
|
bool isTimerActive = false;
|
||||||
|
late Timer markdownInputTimer;
|
||||||
|
late Timer markdownOutputTimer;
|
||||||
|
|
||||||
|
late EditorState editorState;
|
||||||
|
late EditorScrollController scrollController;
|
||||||
|
|
||||||
|
// animation tweens
|
||||||
|
final Map<String, (AnimationController, Animation<double>)> _animations = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
editorState = _parseMarkdown(markdown);
|
||||||
|
scrollController = EditorScrollController(
|
||||||
|
editorState: editorState,
|
||||||
|
shrinkWrap: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
scrollController.dispose();
|
||||||
|
editorState.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
editorScrollController: scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
// the editor is not editable in the chat
|
||||||
|
editable: false,
|
||||||
|
disableAutoScroll: true,
|
||||||
|
editorStyle: const EditorStyle.desktop(),
|
||||||
|
blockWrapper: (
|
||||||
|
context, {
|
||||||
|
required Node node,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (!_animations.containsKey(node.id)) {
|
||||||
|
final controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 2000),
|
||||||
|
);
|
||||||
|
final fade = Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: 1,
|
||||||
|
).animate(controller);
|
||||||
|
_animations[node.id] = (controller, fade);
|
||||||
|
controller.forward();
|
||||||
|
}
|
||||||
|
final (controller, fade) = _animations[node.id]!;
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: fade,
|
||||||
|
builder: (context, childWidget) {
|
||||||
|
return ShaderMask(
|
||||||
|
shaderCallback: (Rect bounds) {
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
stops: [fade.value, fade.value],
|
||||||
|
colors: const [
|
||||||
|
Colors.white,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
blendMode: BlendMode.dstIn,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: fade.value,
|
||||||
|
child: childWidget,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (isTimerActive) {
|
||||||
|
_stopTimer();
|
||||||
|
} else {
|
||||||
|
_startTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: isTimerActive
|
||||||
|
? const Icon(Icons.stop)
|
||||||
|
: const Icon(Icons.play_arrow),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startTimer() {
|
||||||
|
if (isTimerActive) {
|
||||||
|
// stop the timer and reset the offset
|
||||||
|
_stopTimer();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTimerActive = true;
|
||||||
|
|
||||||
|
markdownInputTimer =
|
||||||
|
Timer.periodic(const Duration(milliseconds: 50), (timer) {
|
||||||
|
markdown = _markdown.substring(0, offset);
|
||||||
|
|
||||||
|
offset += 30;
|
||||||
|
|
||||||
|
if (offset >= _markdown.length) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markdownOutputTimer =
|
||||||
|
Timer.periodic(const Duration(milliseconds: 20), (timer) {
|
||||||
|
if (offset >= _markdown.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown = _markdown.substring(0, offset);
|
||||||
|
|
||||||
|
final editorState = _parseMarkdown(
|
||||||
|
markdown,
|
||||||
|
previousDocument: this.editorState.document,
|
||||||
|
);
|
||||||
|
if (editorState.document.last?.id != this.editorState.document.last?.id) {
|
||||||
|
setState(() {
|
||||||
|
this.editorState.dispose();
|
||||||
|
this.editorState = editorState;
|
||||||
|
scrollController.dispose();
|
||||||
|
scrollController = EditorScrollController(
|
||||||
|
editorState: editorState,
|
||||||
|
shrinkWrap: true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopTimer() {
|
||||||
|
if (!isTimerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTimerActive = false;
|
||||||
|
markdownInputTimer.cancel();
|
||||||
|
markdownOutputTimer.cancel();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
markdown = '';
|
||||||
|
offset = 0;
|
||||||
|
final editorState = _parseMarkdown(
|
||||||
|
markdown,
|
||||||
|
previousDocument: this.editorState.document,
|
||||||
|
);
|
||||||
|
this.editorState.dispose();
|
||||||
|
this.editorState = editorState;
|
||||||
|
scrollController.dispose();
|
||||||
|
scrollController = EditorScrollController(
|
||||||
|
editorState: editorState,
|
||||||
|
shrinkWrap: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
_animations.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorState _parseMarkdown(
|
||||||
|
String markdown, {
|
||||||
|
Document? previousDocument,
|
||||||
|
}) {
|
||||||
|
// merge the nodes from the previous document with the new document to keep the same node ids
|
||||||
|
final document = markdownToDocument(markdown);
|
||||||
|
final documentIterator = NodeIterator(
|
||||||
|
document: document,
|
||||||
|
startNode: document.root,
|
||||||
|
);
|
||||||
|
if (previousDocument != null) {
|
||||||
|
final previousDocumentIterator = NodeIterator(
|
||||||
|
document: previousDocument,
|
||||||
|
startNode: previousDocument.root,
|
||||||
|
);
|
||||||
|
while (
|
||||||
|
documentIterator.moveNext() && previousDocumentIterator.moveNext()) {
|
||||||
|
final currentNode = documentIterator.current;
|
||||||
|
final previousNode = previousDocumentIterator.current;
|
||||||
|
if (currentNode.path.equals(previousNode.path)) {
|
||||||
|
currentNode.id = previousNode.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
return editorState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _markdown = '''# AnimationGPT Export Options
|
||||||
|
|
||||||
|
* AnimationGPT typically supports multiple export formats to accommodate different use cases and platforms
|
||||||
|
|
||||||
|
Here's what you should know about exporting animations from AnimationGPT:
|
||||||
|
|
||||||
|
* **Common Export Formats:**
|
||||||
|
* **MP4 video** - Standard video format with good quality and compression
|
||||||
|
* **GIF** - Perfect for simple animations to share on social media or messaging apps
|
||||||
|
* **WebM** - Web-optimized video format that works well for online embedding
|
||||||
|
* **PNG sequence** - Individual frames for further editing in other software
|
||||||
|
|
||||||
|
* **Quality and Resolution Options:**
|
||||||
|
* Most exports allow selecting resolution (720p, 1080p, etc.)
|
||||||
|
* Quality settings to balance file size and visual fidelity
|
||||||
|
* Frame rate options (24fps, 30fps, 60fps) depending on your needs
|
||||||
|
* Some premium tiers may offer higher resolution exports
|
||||||
|
|
||||||
|
* **Platform-Specific Exports:**
|
||||||
|
* Social media optimized presets (Instagram, TikTok, YouTube)
|
||||||
|
* Aspect ratio options (16:9, 1:1, 9:16 for stories/reels)
|
||||||
|
* File size optimization for different platforms
|
||||||
|
|
||||||
|
* **Embedding Options:**
|
||||||
|
* HTML embed codes for websites and blogs
|
||||||
|
* iFrame support for content management systems
|
||||||
|
* API integration for more advanced implementations (may require developer knowledge)
|
||||||
|
|
||||||
|
* **Sharing Features:**
|
||||||
|
* Direct sharing to social media platforms
|
||||||
|
* Shareable links to your animations
|
||||||
|
* Team/collaboration sharing options in some versions
|
||||||
|
|
||||||
|
* **Export Limitations:**
|
||||||
|
* Free tier users may have watermarks or limited export options
|
||||||
|
* Higher resolution exports might require premium subscriptions
|
||||||
|
* Some formats may have file size restrictions
|
||||||
|
|
||||||
|
* **Tips for Exporting:**
|
||||||
|
* Choose GIFs for simple, short animations (under 10 seconds)
|
||||||
|
* Use MP4 for longer animations with sound
|
||||||
|
* Consider your end platform before choosing export settings
|
||||||
|
* Test your animation on the intended platform to ensure it displays correctly
|
||||||
|
|
||||||
|
Most users find the export process straightforward with a simple interface for selecting format, quality, and destination. If you have specific platform requirements, check the export presets to see if AnimationGPT offers optimized settings for your needs."
|
||||||
|
''';
|
||||||
|
|
@ -27,6 +27,7 @@ class PageBlockComponentBuilder extends BlockComponentBuilder {
|
||||||
node: blockComponentContext.node,
|
node: blockComponentContext.node,
|
||||||
header: blockComponentContext.header,
|
header: blockComponentContext.header,
|
||||||
footer: blockComponentContext.footer,
|
footer: blockComponentContext.footer,
|
||||||
|
wrapper: blockComponentContext.wrapper,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -41,10 +42,12 @@ class PageBlockComponent extends BlockComponentStatelessWidget {
|
||||||
super.configuration = const BlockComponentConfiguration(),
|
super.configuration = const BlockComponentConfiguration(),
|
||||||
this.header,
|
this.header,
|
||||||
this.footer,
|
this.footer,
|
||||||
|
this.wrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? header;
|
final Widget? header;
|
||||||
final Widget? footer;
|
final Widget? footer;
|
||||||
|
final BlockComponentWrapper? wrapper;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -64,14 +67,20 @@ class PageBlockComponent extends BlockComponentStatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
if (header != null) header!,
|
if (header != null) header!,
|
||||||
...items.map(
|
...items.map(
|
||||||
(e) => Container(
|
(e) {
|
||||||
|
Widget child = editorState.renderer.build(context, e);
|
||||||
|
if (wrapper != null) {
|
||||||
|
child = wrapper!(context, node: e, child: child);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth:
|
maxWidth:
|
||||||
editorState.editorStyle.maxWidth ?? double.infinity,
|
editorState.editorStyle.maxWidth ?? double.infinity,
|
||||||
),
|
),
|
||||||
padding: editorState.editorStyle.padding,
|
padding: editorState.editorStyle.padding,
|
||||||
child: editorState.renderer.build(context, e),
|
child: child,
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (footer != null) footer!,
|
if (footer != null) footer!,
|
||||||
],
|
],
|
||||||
|
|
@ -102,16 +111,22 @@ class PageBlockComponent extends BlockComponentStatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final node = items[index - (header != null ? 1 : 0)];
|
||||||
|
Widget child = editorState.renderer.build(
|
||||||
|
context,
|
||||||
|
node,
|
||||||
|
);
|
||||||
|
if (wrapper != null) {
|
||||||
|
child = wrapper!(context, node: node, child: child);
|
||||||
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: editorState.editorStyle.maxWidth ?? double.infinity,
|
maxWidth: editorState.editorStyle.maxWidth ?? double.infinity,
|
||||||
),
|
),
|
||||||
padding: editorState.editorStyle.padding,
|
padding: editorState.editorStyle.padding,
|
||||||
child: editorState.renderer.build(
|
child: child,
|
||||||
context,
|
|
||||||
items[index - (header != null ? 1 : 0)],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ class AppFlowyEditor extends StatefulWidget {
|
||||||
this.disableAutoScroll = false,
|
this.disableAutoScroll = false,
|
||||||
this.autoScrollEdgeOffset = appFlowyEditorAutoScrollEdgeOffset,
|
this.autoScrollEdgeOffset = appFlowyEditorAutoScrollEdgeOffset,
|
||||||
this.documentRules = const [],
|
this.documentRules = const [],
|
||||||
|
this.blockWrapper,
|
||||||
}) : blockComponentBuilders =
|
}) : blockComponentBuilders =
|
||||||
blockComponentBuilders ?? standardBlockComponentBuilderMap,
|
blockComponentBuilders ?? standardBlockComponentBuilderMap,
|
||||||
characterShortcutEvents =
|
characterShortcutEvents =
|
||||||
|
|
@ -230,6 +231,11 @@ class AppFlowyEditor extends StatefulWidget {
|
||||||
///
|
///
|
||||||
final List<DocumentRule> documentRules;
|
final List<DocumentRule> documentRules;
|
||||||
|
|
||||||
|
/// Block wrapper
|
||||||
|
///
|
||||||
|
/// Wrap the block component with a widget.
|
||||||
|
final BlockComponentWrapper? blockWrapper;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
||||||
}
|
}
|
||||||
|
|
@ -316,6 +322,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||||
editorState.document.root,
|
editorState.document.root,
|
||||||
header: widget.header,
|
header: widget.header,
|
||||||
footer: widget.footer,
|
footer: widget.footer,
|
||||||
|
wrapper: widget.blockWrapper,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!widget.disableKeyboardService) {
|
if (!widget.disableKeyboardService) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef BlockComponentWrapper = Widget Function(
|
||||||
|
BuildContext context, {
|
||||||
|
required Node node,
|
||||||
|
required Widget child,
|
||||||
|
});
|
||||||
|
|
||||||
class BlockComponentContext {
|
class BlockComponentContext {
|
||||||
const BlockComponentContext(
|
const BlockComponentContext(
|
||||||
this.buildContext,
|
this.buildContext,
|
||||||
this.node, {
|
this.node, {
|
||||||
this.header,
|
this.header,
|
||||||
this.footer,
|
this.footer,
|
||||||
|
this.wrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
final BuildContext buildContext;
|
final BuildContext buildContext;
|
||||||
|
|
@ -15,4 +22,7 @@ class BlockComponentContext {
|
||||||
/// the header and the footer only work for root node.
|
/// the header and the footer only work for root node.
|
||||||
final Widget? header;
|
final Widget? header;
|
||||||
final Widget? footer;
|
final Widget? footer;
|
||||||
|
|
||||||
|
/// Wrap the block component with a widget.
|
||||||
|
final BlockComponentWrapper? wrapper;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ abstract class BlockComponentRendererService {
|
||||||
Node node, {
|
Node node, {
|
||||||
Widget? header,
|
Widget? header,
|
||||||
Widget? footer,
|
Widget? footer,
|
||||||
|
BlockComponentWrapper? wrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Widget> buildList(
|
List<Widget> buildList(
|
||||||
|
|
@ -130,12 +131,14 @@ class BlockComponentRenderer extends BlockComponentRendererService {
|
||||||
Node node, {
|
Node node, {
|
||||||
Widget? header,
|
Widget? header,
|
||||||
Widget? footer,
|
Widget? footer,
|
||||||
|
BlockComponentWrapper? wrapper,
|
||||||
}) {
|
}) {
|
||||||
final blockComponentContext = BlockComponentContext(
|
final blockComponentContext = BlockComponentContext(
|
||||||
buildContext,
|
buildContext,
|
||||||
node,
|
node,
|
||||||
header: header,
|
header: header,
|
||||||
footer: footer,
|
footer: footer,
|
||||||
|
wrapper: wrapper,
|
||||||
);
|
);
|
||||||
final errorBuilder = _builders[errorBlockComponentBuilderKey];
|
final errorBuilder = _builders[errorBlockComponentBuilderKey];
|
||||||
final builder = blockComponentBuilder(node.type);
|
final builder = blockComponentBuilder(node.type);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue