appflowy-editor/example/lib/pages/animated_markdown_page.dart
2025-05-07 10:28:18 +08:00

270 lines
8.2 KiB
Dart

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."
''';