diff --git a/lib/widgets/simple_viewer.dart b/lib/widgets/simple_viewer.dart new file mode 100644 index 00000000..cd3fe5c4 --- /dev/null +++ b/lib/widgets/simple_viewer.dart @@ -0,0 +1,336 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:string_validator/string_validator.dart'; +import 'package:tuple/tuple.dart'; + +import '../models/documents/attribute.dart'; +import '../models/documents/document.dart'; +import '../models/documents/nodes/block.dart'; +import '../models/documents/nodes/leaf.dart' as leaf; +import '../models/documents/nodes/line.dart'; +import 'controller.dart'; +import 'cursor.dart'; +import 'default_styles.dart'; +import 'delegate.dart'; +import 'editor.dart'; +import 'text_block.dart'; +import 'text_line.dart'; + +class QuillSimpleViewer extends StatefulWidget { + const QuillSimpleViewer({ + required this.controller, + this.customStyles, + this.truncate = false, + this.truncateScale, + this.truncateAlignment, + this.truncateHeight, + this.truncateWidth, + this.scrollBottomInset = 0, + this.padding = EdgeInsets.zero, + this.embedBuilder, + Key? key, + }) : assert(truncate || + ((truncateScale == null) && + (truncateAlignment == null) && + (truncateHeight == null) && + (truncateWidth == null))), + super(key: key); + + final QuillController controller; + final DefaultStyles? customStyles; + final bool truncate; + final double? truncateScale; + final Alignment? truncateAlignment; + final double? truncateHeight; + final double? truncateWidth; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final EmbedBuilder? embedBuilder; + + @override + _QuillSimpleViewerState createState() => _QuillSimpleViewerState(); +} + +class _QuillSimpleViewerState extends State + with SingleTickerProviderStateMixin { + late DefaultStyles _styles; + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + late CursorCont _cursorCont; + + @override + void initState() { + super.initState(); + + _cursorCont = CursorCont( + show: ValueNotifier(false), + style: const CursorStyle( + color: Colors.black, + backgroundColor: Colors.grey, + width: 2, + radius: Radius.zero, + offset: Offset.zero, + ), + tickerProvider: this, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; + + if (widget.customStyles != null) { + _styles = _styles.merge(widget.customStyles!); + } + } + + EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; + + Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { + assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); + switch (node.value.type) { + case 'image': + final imageUrl = _standardizeImageUrl(node.value.data); + return imageUrl.startsWith('http') + ? Image.network(imageUrl) + : isBase64(imageUrl) + ? Image.memory(base64.decode(imageUrl)) + : Image.file(io.File(imageUrl)); + default: + throw UnimplementedError( + 'Embeddable type "${node.value.type}" is not supported by default embed ' + 'builder of QuillEditor. You must pass your own builder function to ' + 'embedBuilder property of QuillEditor or QuillField widgets.'); + } + } + + String _standardizeImageUrl(String url) { + if (url.contains('base64')) { + return url.split(',')[1]; + } + return url; + } + + @override + Widget build(BuildContext context) { + final _doc = widget.controller.document; + // if (_doc.isEmpty() && + // !widget.focusNode.hasFocus && + // widget.placeholder != null) { + // _doc = Document.fromJson(jsonDecode( + // '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); + // } + + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: _SimpleViewer( + document: _doc, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _nullSelectionChanged, + scrollBottomInset: widget.scrollBottomInset, + padding: widget.padding, + children: _buildChildren(_doc, context), + ), + ), + ); + + if (widget.truncate) { + child = Container( + height: widget.truncateHeight, + width: widget.truncateWidth, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + controller: ScrollController(), + child: child), + ); + if (widget.truncateScale != null) { + child = Align( + heightFactor: widget.truncateScale, + widthFactor: widget.truncateScale, + alignment: widget.truncateAlignment ?? Alignment.topLeft, + child: Transform.scale( + scale: widget.truncateScale!, + alignment: widget.truncateAlignment ?? Alignment.topLeft, + child: child), + ); + } + } + + return QuillStyles( + data: _styles, + child: child, + ); + } + + List _buildChildren(Document doc, BuildContext context) { + final result = []; + final indentLevelCounts = {}; + for (final node in doc.root.children) { + if (node is Line) { + final editableTextLine = _getEditableTextLineFromNode(node, context); + result.add(editableTextLine); + } else if (node is Block) { + final attrs = node.style.attributes; + final editableTextBlock = EditableTextBlock( + node, + _textDirection, + widget.scrollBottomInset, + _getVerticalSpacingForBlock(node, _styles), + widget.controller.selection, + Colors.black, + // selectionColor, + _styles, + false, + // enableInteractiveSelection, + false, + // hasFocus, + attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + embedBuilder, + _cursorCont, + indentLevelCounts); + result.add(editableTextBlock); + } else { + throw StateError('Unreachable.'); + } + } + return result; + } + + TextDirection get _textDirection { + final result = Directionality.of(context); + return result; + } + + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + final textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: embedBuilder, + styles: _styles, + ); + final editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + widget.controller.selection, + Colors.black, + //widget.selectionColor, + false, + //enableInteractiveSelection, + false, + //_hasFocus, + MediaQuery.of(context).devicePixelRatio, + _cursorCont); + return editableTextLine; + } + + Tuple2 _getVerticalSpacingForLine( + Line line, DefaultStyles? defaultStyles) { + final attrs = line.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + final int? level = attrs[Attribute.header.key]!.value; + switch (level) { + case 1: + return defaultStyles!.h1!.verticalSpacing; + case 2: + return defaultStyles!.h2!.verticalSpacing; + case 3: + return defaultStyles!.h3!.verticalSpacing; + default: + throw 'Invalid level $level'; + } + } + + return defaultStyles!.paragraph!.verticalSpacing; + } + + Tuple2 _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = node.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.verticalSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.verticalSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + return defaultStyles!.indent!.verticalSpacing; + } + return defaultStyles!.lists!.verticalSpacing; + } + + void _nullSelectionChanged( + TextSelection selection, SelectionChangedCause cause) {} +} + +class _SimpleViewer extends MultiChildRenderObjectWidget { + _SimpleViewer({ + required List children, + required this.document, + required this.textDirection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, + required this.scrollBottomInset, + this.padding = EdgeInsets.zero, + Key? key, + }) : super(key: key, children: children); + + final Document document; + final TextDirection textDirection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + null, + textDirection, + scrollBottomInset, + padding, + document, + const TextSelection(baseOffset: 0, extentOffset: 0), + false, + // hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + const EdgeInsets.fromLTRB(4, 4, 4, 5), + ); + } + + @override + void updateRenderObject( + BuildContext context, covariant RenderEditor renderObject) { + renderObject + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) + ..setPadding(padding); + } +}