您最多选择25个主题 主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

525 行
18 KiB

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<LinkPreview>
{
public Animation<float> _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<Widget>();
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<Widget>();
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<Widget>();
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<Widget>();
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<Widget>();
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<Widget>();
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<Widget> {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
);
}
}
}