您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
525 行
18 KiB
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
|
|
);
|
|
}
|
|
}
|
|
}
|