using System; using System.Collections.Generic; using System.Text.RegularExpressions; using ChatComponents; using JetBrains.Annotations; using Unity.UIWidgets.animation; using Unity.UIWidgets.async; using Unity.UIWidgets.foundation; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using Image = Unity.UIWidgets.widgets.Image; using TextStyle = Unity.UIWidgets.painting.TextStyle; namespace UIWidgetsSample { public delegate void OnLinkPressed(string str); public class LinkPreview : StatefulWidget { /// Expand animation duration public readonly TimeSpan? animationDuration; /// Enables expand animation. Default value is false. public readonly bool? enableAnimation; /// Custom header above provided text public readonly string header; /// Style of the custom header public readonly TextStyle headerStyle; /// Style of highlighted links in the text public readonly TextStyle linkStyle; /// Style of preview's description public readonly TextStyle metadataTextStyle; /// Style of preview's title public readonly TextStyle metadataTitleStyle; /// Custom link press handler public readonly OnLinkPressed onLinkPressed; /// Callback which is called when [PreviewData] was successfully parsed. /// Use it to save [PreviewData] to the state and pass it back /// to the [LinkPreview.previewData] so the [LinkPreview] would not fetch /// preview data again. public readonly OnPreviewDataFetched onPreviewDataFetched; /// Padding around initial text widget public readonly EdgeInsets padding; /// Pass saved [PreviewData] here so [LinkPreview] would not fetch preview /// data again public readonly PreviewData previewData; /// Text used for parsing public readonly string text; /// Style of the provided text public readonly TextStyle textStyle; /// Width of the [LinkPreview] widget public readonly float width; /// Creates [LinkPreview] public LinkPreview( OnPreviewDataFetched onPreviewDataFetched, PreviewData previewData, string text, float width, Key key = null, TimeSpan? animationDuration = null, bool enableAnimation = false, [CanBeNull] string header = null, TextStyle headerStyle = null, TextStyle linkStyle = null, TextStyle metadataTextStyle = null, TextStyle metadataTitleStyle = null, OnLinkPressed onLinkPressed = null, [CanBeNull] EdgeInsets padding = null, TextStyle textStyle = null ) : base(key) { this.onPreviewDataFetched = onPreviewDataFetched; this.previewData = previewData; this.text = text; this.width = width; this.animationDuration = animationDuration; this.enableAnimation = enableAnimation; this.header = header; this.headerStyle = headerStyle; this.linkStyle = linkStyle; this.metadataTextStyle = metadataTextStyle; this.metadataTitleStyle = metadataTitleStyle; this.onLinkPressed = onLinkPressed; this.padding = padding; this.textStyle = textStyle; } public override State createState() { return new _LinkPreviewState(); } } public class _LinkPreviewState : SingleTickerProviderStateMixin { public Animation _animation; public AnimationController _controller; private bool isFetchingPreviewData; private bool shouldAnimate; public override void initState() { base.initState(); didUpdateWidget(widget); _controller = new AnimationController( duration: widget.animationDuration ?? TimeSpan.FromMilliseconds(300), vsync: this); _animation = new CurvedAnimation( parent: _controller, curve: Curves.easeOutQuad ); } public override void didUpdateWidget(StatefulWidget oldWidget) { oldWidget = (LinkPreview) oldWidget; base.didUpdateWidget(oldWidget); if (!isFetchingPreviewData && widget.previewData == null) _fetchData(widget.text); if (widget.previewData != null && ((LinkPreview) oldWidget).previewData == null) { setState(() => { shouldAnimate = true; }); _controller.reset(); _controller.forward(); } else if (widget.previewData != null) { setState(() => { shouldAnimate = false; }); } } public override void dispose() { _controller.dispose(); base.dispose(); } private PreviewData _fetchData(string text) { setState(() => { isFetchingPreviewData = true; }); var previewData = getPreviewData(text); _handlePreviewDataFetched(previewData); return previewData; } private void _handlePreviewDataFetched(PreviewData previewData) { Future.delayed( widget.animationDuration ?? TimeSpan.FromMilliseconds(300) ); if (mounted) { widget.onPreviewDataFetched(previewData); setState(() => { isFetchingPreviewData = false; }); } } private bool _hasData(PreviewData previewData) { return previewData?.title != null || previewData?.description != null || previewData?.image?.url != null; } private bool _hasOnlyImage() { return widget.previewData?.title == null && widget.previewData?.description == null && widget.previewData?.image?.url != null; } /*private Future _onOpen(LinkableElement link) { if (canLaunch(link.url)) launch(link.url); else throw $"Could not launch {link}"; }*/ private Widget _animated(Widget child) { return new SizeTransition( axis: Axis.vertical, axisAlignment: -1, sizeFactor: _animation, child: child ); } private Widget _bodyWidget(PreviewData data, string text, float width) { var _padding = widget.padding ?? EdgeInsets.only( bottom: 16, left: 24, right: 24 ); var results = new List(); if (data.title != null) results.Add(_titleWidget((string)data.title )); if (data.description != null) results.Add(_descriptionWidget((string)data.description)); var final_results = new List(); final_results.Add(new Container( padding: EdgeInsets.only( bottom: _padding.bottom, left: _padding.left, right: _padding.right ), child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: results ) )); if (data.image?.url != null) final_results.Add(_imageWidget(data.image.url, width)); return new Column( crossAxisAlignment: CrossAxisAlignment.start, children: final_results ); } private Widget _containerWidget( bool animate, bool withPadding = false, Widget child = null ) { var _padding = widget.padding ?? EdgeInsets.symmetric( horizontal: 24, vertical: 16 ); var shouldAnimate = widget.enableAnimation == true && animate; var results = new List(); if (widget.header != null) results.Add(new Padding( padding: EdgeInsets.only(bottom: 6), child: new Text( (string)widget.header, maxLines: 1, overflow: TextOverflow.ellipsis, style: widget.headerStyle ) )); //results.Add(_linkify()); if (withPadding && child != null) results.Add(shouldAnimate ? _animated(child) : child); var final_results = new List(); final_results.Add(new Padding( padding: withPadding ? EdgeInsets.all(0) : EdgeInsets.only( _padding.left, right: _padding.right, top: _padding.top, bottom: _hasOnlyImage() ? 0 : 16 ), child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: results ) )); if (!withPadding && child != null) final_results.Add(shouldAnimate ? _animated(child) : child); return new Container( constraints: new BoxConstraints(maxWidth: widget.width), padding: withPadding ? _padding : null, child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: final_results ) ); } private Widget _descriptionWidget(string description) { return new Container( margin: EdgeInsets.only(top: 8), child: new Text( description, maxLines: 3, overflow: TextOverflow.ellipsis, style: widget.metadataTextStyle ) ); } private Widget _imageWidget(string url, float width) { return new Container( constraints: new BoxConstraints( maxHeight: width ), width: width, child: Image.network( url, fit: BoxFit.fitWidth ) ); } /*private Widget _linkify() { return SelectableLinkify( linkifiers:[UrlLinkifier()], widget.linkStyle, 100, 1, widget.onLinkPressed != null ? element => widget.onLinkPressed!(element.url) : _onOpen, LinkifyOptions( defaultToHttps: true, humanize: false, looseUrl: true, ), widget.text, widget.textStyle ); }*/ private Widget _minimizedBodyWidget(PreviewData data, string text) { var inner_results = new List(); if (data.title != null) inner_results.Add(_titleWidget(data.title)); if (data.description != null) inner_results.Add(_descriptionWidget(data.description)); if (data.image.url != null) _minimizedImageWidget(data.image.url); var results = new List(); results.Add(new Expanded( child: new Container( margin: EdgeInsets.only(right: 4), child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: inner_results ) ) )); if (data.image.url != null) results.Add(_minimizedImageWidget(data.image.url)); var outerResults = (Widget) null; if (data.title != null || data.description != null) outerResults = new Container( margin: EdgeInsets.only(top: 16), child: new Row( crossAxisAlignment: CrossAxisAlignment.start, children: results ) ); return new Column( crossAxisAlignment: CrossAxisAlignment.start, children: new List {outerResults} ); } private Widget _minimizedImageWidget(string url) { return new ClipRRect( borderRadius: BorderRadius.all( Radius.circular(12) ), child: new SizedBox( height: 48, width: 48, child: Image.network(url) ) ); } private Widget _titleWidget(string title) { var style = widget.metadataTitleStyle ?? new TextStyle( fontWeight: FontWeight.bold ); return new Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, style: style ); } public override Widget build(BuildContext context) { if (widget.previewData != null && _hasData(widget.previewData)) { var aspectRatio = widget.previewData.image == null ? 0f : widget.previewData.image.width / widget.previewData.image.height; var _width = aspectRatio == 1 ? widget.width : widget.width - 32; return _containerWidget( shouldAnimate, child: aspectRatio == 1 ? _minimizedBodyWidget(widget.previewData, widget.text) : _bodyWidget(widget.previewData, widget.text, _width), withPadding: aspectRatio == 1 ); } return _containerWidget(false); } private PreviewData getPreviewData(string text) { var previewData = new PreviewData(); string previewDataDescription = null; PreviewDataImage previewDataImage = null; string previewDataTitle =""; string previewDataUrl =""; var REGEX_LINK = @"([\w+]+\:\/\/)?([\w\d-]+\.)*[\w-]+[\.\:]\w+([\/\?\=\&\#\.]?[\w-]+)*\/?"; var REGEX_IMAGE_CONTENT_TYPE = @"image\/*"; /* try { var urlRegexp = new Regex(REGEX_LINK); var matches = urlRegexp.Match(text.ToLower()); if (matches.Length == 0) return previewData; var url = text.Substring(matches.first.start, matches.first.end); if (!url.ToLower().StartsWith("http")) url = "https://" + url; previewDataUrl = url; var uri = Uri.parse(url); var response = http.get(uri); var document = parser.parse(response.body); var imageRegexp = new Regex(REGEX_IMAGE_CONTENT_TYPE); if (imageRegexp.hasMatch(response.headers["content-type"] ?? "")) { var imageSize = _getImageSize(previewDataUrl); previewDataImage = new PreviewDataImage( imageSize.height, previewDataUrl, imageSize.width ); return new PreviewData( image: previewDataImage, link: previewDataUrl ); } if (!_hasUTF8Charset(document)) return previewData; var title = _getTitle(document); if (title != null) previewDataTitle = title.trim(); var description = _getDescription(document); if (description != null) previewDataDescription = description.trim(); var imageUrls = _getImageUrls(document, url); Size imageSize; string imageUrl; if (imageUrls.isNotEmpty) { imageUrl = imageUrls.length == 1 ? imageUrls[0] : _getBiggestImageUrl(imageUrls); imageSize = _getImageSize(imageUrl); previewDataImage = new PreviewDataImage( imageSize.height, imageUrl, imageSize.width ); } return new PreviewData( previewDataDescription, previewDataImage, previewDataUrl, previewDataTitle ); } catch (Exception e) { return new PreviewData( previewDataDescription, previewDataImage, previewDataUrl, previewDataTitle ); }*/ return new PreviewData( previewDataDescription, previewDataImage, previewDataUrl, previewDataTitle ); } } }