Yuncong Zhang
6 年前
当前提交
9b1bba59
共有 5 个文件被更改,包括 658 次插入 和 1 次删除
-
2Runtime/rendering/proxy_box.cs
-
2Runtime/widgets/automatic_keep_alive.cs
-
2Runtime/widgets/ticker_provider.cs
-
650Runtime/widgets/dismissible.cs
-
3Runtime/widgets/dismissible.cs.meta
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.UIWidgets.animation; |
|||
using Unity.UIWidgets.foundation; |
|||
using Unity.UIWidgets.gestures; |
|||
using Unity.UIWidgets.painting; |
|||
using Unity.UIWidgets.rendering; |
|||
using Unity.UIWidgets.scheduler; |
|||
using Unity.UIWidgets.ui; |
|||
|
|||
namespace Unity.UIWidgets.widgets { |
|||
public delegate void DismissDirectionCallback(DismissDirection? direction); |
|||
|
|||
public enum DismissDirection { |
|||
vertical, |
|||
|
|||
horizontal, |
|||
|
|||
endToStart, |
|||
|
|||
startToEnd, |
|||
|
|||
up, |
|||
|
|||
down |
|||
} |
|||
|
|||
public class Dismissible : StatefulWidget { |
|||
public Dismissible( |
|||
Key key = null, |
|||
Widget child = null, |
|||
Widget background = null, |
|||
Widget secondaryBackground = null, |
|||
VoidCallback onResize = null, |
|||
DismissDirectionCallback onDismissed = null, |
|||
DismissDirection direction = DismissDirection.horizontal, |
|||
TimeSpan? resizeDuration = null, |
|||
Dictionary<DismissDirection?, float?> dismissThresholds = null, |
|||
TimeSpan? movementDuration = null, |
|||
float crossAxisEndOffset = 0.0f |
|||
) : base(key: key) { |
|||
D.assert(key != null); |
|||
D.assert(secondaryBackground != null ? background != null : true); |
|||
this.resizeDuration = resizeDuration ?? new TimeSpan(0, 0, 0, 0, 300); |
|||
this.dismissThresholds = dismissThresholds ?? new Dictionary<DismissDirection?, float?>(); |
|||
this.movementDuration = movementDuration ?? new TimeSpan(0, 0, 0, 0, 200); |
|||
} |
|||
|
|||
public readonly Widget child; |
|||
|
|||
public readonly Widget background; |
|||
|
|||
public readonly Widget secondaryBackground; |
|||
|
|||
public readonly VoidCallback onResize; |
|||
|
|||
public readonly DismissDirectionCallback onDismissed; |
|||
|
|||
public readonly DismissDirection direction; |
|||
|
|||
public readonly TimeSpan resizeDuration; |
|||
|
|||
public readonly Dictionary<DismissDirection?, float?> dismissThresholds; |
|||
|
|||
public readonly TimeSpan movementDuration; |
|||
|
|||
public readonly float crossAxisEndOffset; |
|||
|
|||
public override State createState() { |
|||
return new _DismissibleState(); |
|||
} |
|||
} |
|||
|
|||
public abstract class AutomaticKeepAliveClientWithTickerProviderStateMixin<T> : State<T>, TickerProvider |
|||
where T : StatefulWidget { |
|||
HashSet<Ticker> _tickers; |
|||
|
|||
public Ticker createTicker(TickerCallback onTick) { |
|||
this._tickers = this._tickers ?? new HashSet<Ticker>(); |
|||
var result = new _AutomaticWidgetTicker<T>(onTick, this, debugLabel: "created by " + this); |
|||
this._tickers.Add(result); |
|||
return result; |
|||
} |
|||
|
|||
internal void _removeTicker(_AutomaticWidgetTicker<T> ticker) { |
|||
D.assert(this._tickers != null); |
|||
D.assert(this._tickers.Contains(ticker)); |
|||
this._tickers.Remove(ticker); |
|||
} |
|||
|
|||
public override void dispose() { |
|||
D.assert(() => { |
|||
if (this._tickers != null) { |
|||
foreach (Ticker ticker in this._tickers) { |
|||
if (ticker.isActive) { |
|||
throw new UIWidgetsError( |
|||
this + " was disposed with an active Ticker.\n" + |
|||
this.GetType() + |
|||
" created a Ticker via its TickerProviderStateMixin, but at the time " + |
|||
"dispose() was called on the mixin, that Ticker was still active. All Tickers must " + |
|||
"be disposed before calling super.dispose(). Tickers used by AnimationControllers " + |
|||
"should be disposed by calling dispose() on the AnimationController itself. " + |
|||
"Otherwise, the ticker will leak.\n" + |
|||
"The offending ticker was: " + ticker.toString(debugIncludeStack: true) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
}); |
|||
base.dispose(); |
|||
} |
|||
|
|||
public override void didChangeDependencies() { |
|||
bool muted = !TickerMode.of(this.context); |
|||
if (this._tickers != null) { |
|||
foreach (Ticker ticker in this._tickers) { |
|||
ticker.muted = muted; |
|||
} |
|||
} |
|||
|
|||
base.didChangeDependencies(); |
|||
} |
|||
|
|||
public override void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
|||
base.debugFillProperties(properties); |
|||
properties.add(new DiagnosticsProperty<HashSet<Ticker>>( |
|||
"tickers", |
|||
this._tickers, |
|||
description: this._tickers != null ? "tracking " + this._tickers.Count + " tickers" : null, |
|||
defaultValue: Diagnostics.kNullDefaultValue |
|||
)); |
|||
} |
|||
|
|||
KeepAliveHandle _keepAliveHandle; |
|||
|
|||
void _ensureKeepAlive() { |
|||
D.assert(this._keepAliveHandle == null); |
|||
this._keepAliveHandle = new KeepAliveHandle(); |
|||
new KeepAliveNotification(this._keepAliveHandle).dispatch(this.context); |
|||
} |
|||
|
|||
void _releaseKeepAlive() { |
|||
this._keepAliveHandle.release(); |
|||
this._keepAliveHandle = null; |
|||
} |
|||
|
|||
protected abstract bool wantKeepAlive { get; } |
|||
|
|||
protected void updateKeepAlive() { |
|||
if (this.wantKeepAlive) { |
|||
if (this._keepAliveHandle == null) { |
|||
this._ensureKeepAlive(); |
|||
} |
|||
} |
|||
else { |
|||
if (this._keepAliveHandle != null) { |
|||
this._releaseKeepAlive(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
if (this.wantKeepAlive) { |
|||
this._ensureKeepAlive(); |
|||
} |
|||
} |
|||
|
|||
public override void deactivate() { |
|||
if (this._keepAliveHandle != null) { |
|||
this._releaseKeepAlive(); |
|||
} |
|||
|
|||
base.deactivate(); |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
if (this.wantKeepAlive && this._keepAliveHandle == null) { |
|||
this._ensureKeepAlive(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
|
|||
class _AutomaticWidgetTicker<T> : Ticker where T : StatefulWidget { |
|||
internal _AutomaticWidgetTicker( |
|||
TickerCallback onTick, |
|||
AutomaticKeepAliveClientWithTickerProviderStateMixin<T> creator, |
|||
string debugLabel = null) : |
|||
base(onTick: onTick, debugLabel: debugLabel) { |
|||
this._creator = creator; |
|||
} |
|||
|
|||
readonly AutomaticKeepAliveClientWithTickerProviderStateMixin<T> _creator; |
|||
|
|||
public override void dispose() { |
|||
this._creator._removeTicker(this); |
|||
base.dispose(); |
|||
} |
|||
} |
|||
|
|||
class _DismissibleClipper : CustomClipper<Rect> { |
|||
public _DismissibleClipper( |
|||
Axis axis, |
|||
Animation<Offset> moveAnimation |
|||
) : base(reclip: moveAnimation) { |
|||
D.assert(moveAnimation != null); |
|||
this.axis = axis; |
|||
this.moveAnimation = moveAnimation; |
|||
} |
|||
|
|||
public readonly Axis axis; |
|||
public readonly Animation<Offset> moveAnimation; |
|||
|
|||
public override Rect getClip(Size size) { |
|||
switch (this.axis) { |
|||
case Axis.horizontal: |
|||
float offset1 = this.moveAnimation.value.dx * size.width; |
|||
if (offset1 < 0) { |
|||
return Rect.fromLTRB(size.width + offset1, 0.0f, size.width, size.height); |
|||
} |
|||
|
|||
return Rect.fromLTRB(0.0f, 0.0f, offset1, size.height); |
|||
case Axis.vertical: |
|||
float offset = this.moveAnimation.value.dy * size.height; |
|||
if (offset < 0) { |
|||
return Rect.fromLTRB(0.0f, size.height + offset, size.width, size.height); |
|||
} |
|||
|
|||
return Rect.fromLTRB(0.0f, 0.0f, size.width, offset); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public override Rect getApproximateClipRect(Size size) { |
|||
return this.getClip(size); |
|||
} |
|||
|
|||
public override bool shouldReclip(CustomClipper<Rect> oldClipper) { |
|||
D.assert(oldClipper is _DismissibleClipper); |
|||
_DismissibleClipper clipper = oldClipper as _DismissibleClipper; |
|||
return clipper.axis != this.axis |
|||
|| clipper.moveAnimation.value != this.moveAnimation.value; |
|||
} |
|||
} |
|||
|
|||
enum _FlingGestureKind { |
|||
none, |
|||
forward, |
|||
reverse |
|||
} |
|||
|
|||
public class _DismissibleState : AutomaticKeepAliveClientWithTickerProviderStateMixin<Dismissible> { |
|||
static readonly Curve _kResizeTimeCurve = new Interval(0.4f, 1.0f, curve: Curves.ease); |
|||
const float _kMinFlingVelocity = 700.0f; |
|||
const float _kMinFlingVelocityDelta = 400.0f; |
|||
const float _kFlingVelocityScale = 1.0f / 300.0f; |
|||
const float _kDismissThreshold = 0.4f; |
|||
|
|||
public override void initState() { |
|||
base.initState(); |
|||
this._moveController = new AnimationController(duration: this.widget.movementDuration, vsync: this); |
|||
this._moveController.addStatusListener(this._handleDismissStatusChanged); |
|||
this._updateMoveAnimation(); |
|||
} |
|||
|
|||
AnimationController _moveController; |
|||
Animation<Offset> _moveAnimation; |
|||
|
|||
AnimationController _resizeController; |
|||
Animation<float> _resizeAnimation; |
|||
|
|||
float _dragExtent = 0.0f; |
|||
bool _dragUnderway = false; |
|||
Size _sizePriorToCollapse; |
|||
|
|||
protected override bool wantKeepAlive { |
|||
get { return this._moveController?.isAnimating == true || this._resizeController?.isAnimating == true; } |
|||
} |
|||
|
|||
public override void dispose() { |
|||
this._moveController.dispose(); |
|||
this._resizeController?.dispose(); |
|||
base.dispose(); |
|||
} |
|||
|
|||
bool _directionIsXAxis { |
|||
get { |
|||
return this.widget.direction == DismissDirection.horizontal |
|||
|| this.widget.direction == DismissDirection.endToStart |
|||
|| this.widget.direction == DismissDirection.startToEnd; |
|||
} |
|||
} |
|||
|
|||
DismissDirection? _extentToDirection(float? extent) { |
|||
if (extent == 0.0) { |
|||
return null; |
|||
} |
|||
|
|||
if (this._directionIsXAxis) { |
|||
switch (Directionality.of(this.context)) { |
|||
case TextDirection.rtl: |
|||
return extent < 0 ? DismissDirection.startToEnd : DismissDirection.endToStart; |
|||
case TextDirection.ltr: |
|||
return extent > 0 ? DismissDirection.startToEnd : DismissDirection.endToStart; |
|||
} |
|||
|
|||
D.assert(false); |
|||
return null; |
|||
} |
|||
|
|||
return extent > 0 ? DismissDirection.down : DismissDirection.up; |
|||
} |
|||
|
|||
DismissDirection? _dismissDirection { |
|||
get { return this._extentToDirection(this._dragExtent); } |
|||
} |
|||
|
|||
bool _isActive { |
|||
get { return this._dragUnderway || this._moveController.isAnimating; } |
|||
} |
|||
|
|||
float _overallDragAxisExtent { |
|||
get { |
|||
Size size = this.context.size; |
|||
return this._directionIsXAxis ? size.width : size.height; |
|||
} |
|||
} |
|||
|
|||
void _handleDragStart(DragStartDetails details) { |
|||
this._dragUnderway = true; |
|||
if (this._moveController.isAnimating) { |
|||
this._dragExtent = this._moveController.value * this._overallDragAxisExtent * this._dragExtent.sign(); |
|||
this._moveController.stop(); |
|||
} |
|||
else { |
|||
this._dragExtent = 0.0f; |
|||
this._moveController.setValue(0.0f); |
|||
} |
|||
|
|||
this.setState(() => { this._updateMoveAnimation(); }); |
|||
} |
|||
|
|||
void _handleDragUpdate(DragUpdateDetails details) { |
|||
if (!this._isActive || this._moveController.isAnimating) { |
|||
return; |
|||
} |
|||
|
|||
float delta = details.primaryDelta ?? 0.0f; |
|||
float oldDragExtent = this._dragExtent; |
|||
switch (this.widget.direction) { |
|||
case DismissDirection.horizontal: |
|||
case DismissDirection.vertical: |
|||
this._dragExtent += delta; |
|||
break; |
|||
|
|||
case DismissDirection.up: |
|||
if (this._dragExtent + delta < 0) { |
|||
this._dragExtent += delta; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case DismissDirection.down: |
|||
if (this._dragExtent + delta > 0) { |
|||
this._dragExtent += delta; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case DismissDirection.endToStart: |
|||
switch (Directionality.of(this.context)) { |
|||
case TextDirection.rtl: |
|||
if (this._dragExtent + delta > 0) { |
|||
this._dragExtent += delta; |
|||
} |
|||
|
|||
break; |
|||
case TextDirection.ltr: |
|||
if (this._dragExtent + delta < 0) { |
|||
this._dragExtent += delta; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case DismissDirection.startToEnd: |
|||
switch (Directionality.of(this.context)) { |
|||
case TextDirection.rtl: |
|||
if (this._dragExtent + delta < 0) { |
|||
this._dragExtent += delta; |
|||
} |
|||
|
|||
break; |
|||
case TextDirection.ltr: |
|||
if (this._dragExtent + delta > 0) { |
|||
this._dragExtent += delta; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
if (oldDragExtent.sign() != this._dragExtent.sign()) { |
|||
this.setState(() => { this._updateMoveAnimation(); }); |
|||
} |
|||
|
|||
if (!this._moveController.isAnimating) { |
|||
this._moveController.setValue(this._dragExtent.abs() / this._overallDragAxisExtent); |
|||
} |
|||
} |
|||
|
|||
void _updateMoveAnimation() { |
|||
float end = this._dragExtent.sign(); |
|||
this._moveAnimation = this._moveController.drive( |
|||
new OffsetTween( |
|||
begin: Offset.zero, |
|||
end: this._directionIsXAxis |
|||
? new Offset(end, this.widget.crossAxisEndOffset) |
|||
: new Offset(this.widget.crossAxisEndOffset, end) |
|||
) |
|||
); |
|||
} |
|||
|
|||
_FlingGestureKind _describeFlingGesture(Velocity velocity) { |
|||
if (this._dragExtent == 0.0f) { |
|||
return _FlingGestureKind.none; |
|||
} |
|||
|
|||
float vx = velocity.pixelsPerSecond.dx; |
|||
float vy = velocity.pixelsPerSecond.dy; |
|||
DismissDirection? flingDirection; |
|||
if (this._directionIsXAxis) { |
|||
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || vx.abs() < _kMinFlingVelocity) { |
|||
return _FlingGestureKind.none; |
|||
} |
|||
|
|||
D.assert(vx != 0.0f); |
|||
flingDirection = this._extentToDirection(vx); |
|||
} |
|||
else { |
|||
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta || vy.abs() < _kMinFlingVelocity) { |
|||
return _FlingGestureKind.none; |
|||
} |
|||
|
|||
D.assert(vy != 0.0); |
|||
flingDirection = this._extentToDirection(vy); |
|||
} |
|||
|
|||
D.assert(this._dismissDirection != null); |
|||
if (flingDirection == this._dismissDirection) { |
|||
return _FlingGestureKind.forward; |
|||
} |
|||
|
|||
return _FlingGestureKind.reverse; |
|||
} |
|||
|
|||
void _handleDragEnd(DragEndDetails details) { |
|||
if (!this._isActive || this._moveController.isAnimating) { |
|||
return; |
|||
} |
|||
|
|||
this._dragUnderway = false; |
|||
if (this._moveController.isCompleted) { |
|||
this._startResizeAnimation(); |
|||
return; |
|||
} |
|||
|
|||
float flingVelocity = this._directionIsXAxis |
|||
? details.velocity.pixelsPerSecond.dx |
|||
: details.velocity.pixelsPerSecond.dy; |
|||
switch (this._describeFlingGesture(details.velocity)) { |
|||
case _FlingGestureKind.forward: |
|||
D.assert(this._dragExtent != 0.0f); |
|||
D.assert(!this._moveController.isDismissed); |
|||
if ((this.widget.dismissThresholds[this._dismissDirection] ?? _kDismissThreshold) >= 1.0) { |
|||
this._moveController.reverse(); |
|||
break; |
|||
} |
|||
|
|||
this._dragExtent = flingVelocity.sign(); |
|||
this._moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); |
|||
break; |
|||
case _FlingGestureKind.reverse: |
|||
D.assert(this._dragExtent != 0.0); |
|||
D.assert(!this._moveController.isDismissed); |
|||
this._dragExtent = flingVelocity.sign(); |
|||
this._moveController.fling(velocity: -flingVelocity.abs() * _kFlingVelocityScale); |
|||
break; |
|||
case _FlingGestureKind.none: |
|||
if (!this._moveController.isDismissed) { |
|||
// we already know it's not completed, we check that above
|
|||
if (this._moveController.value > |
|||
(this.widget.dismissThresholds[this._dismissDirection] ?? _kDismissThreshold)) { |
|||
this._moveController.forward(); |
|||
} |
|||
else { |
|||
this._moveController.reverse(); |
|||
} |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
void _handleDismissStatusChanged(AnimationStatus status) { |
|||
if (status == AnimationStatus.completed && !this._dragUnderway) { |
|||
this._startResizeAnimation(); |
|||
} |
|||
|
|||
this.updateKeepAlive(); |
|||
} |
|||
|
|||
void _startResizeAnimation() { |
|||
D.assert(this._moveController != null); |
|||
D.assert(this._moveController.isCompleted); |
|||
D.assert(this._resizeController == null); |
|||
D.assert(this._sizePriorToCollapse == null); |
|||
if (this.widget.resizeDuration == null) { |
|||
if (this.widget.onDismissed != null) { |
|||
DismissDirection? direction = this._dismissDirection; |
|||
D.assert(direction != null); |
|||
this.widget.onDismissed(direction); |
|||
} |
|||
} |
|||
else { |
|||
this._resizeController = new AnimationController(duration: this.widget.resizeDuration, vsync: this); |
|||
this._resizeController.addListener(this._handleResizeProgressChanged); |
|||
this._resizeController.addStatusListener((AnimationStatus status) => this.updateKeepAlive()); |
|||
this._resizeController.forward(); |
|||
this.setState(() => { |
|||
this._sizePriorToCollapse = this.context.size; |
|||
this._resizeAnimation = this._resizeController.drive( |
|||
new CurveTween( |
|||
curve: _kResizeTimeCurve |
|||
) |
|||
).drive( |
|||
new FloatTween( |
|||
begin: 1.0f, |
|||
end: 0.0f |
|||
) |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
void _handleResizeProgressChanged() { |
|||
if (this._resizeController.isCompleted) { |
|||
if (this.widget.onDismissed != null) { |
|||
DismissDirection? direction = this._dismissDirection; |
|||
D.assert(direction != null); |
|||
this.widget.onDismissed(direction); |
|||
} |
|||
} |
|||
else { |
|||
if (this.widget.onResize != null) { |
|||
this.widget.onResize(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public override Widget build(BuildContext context) { |
|||
base.build(context); // See AutomaticKeepAliveClientMixin.
|
|||
|
|||
D.assert(!this._directionIsXAxis || WidgetsD.debugCheckHasDirectionality(context)); |
|||
|
|||
Widget background = this.widget.background; |
|||
if (this.widget.secondaryBackground != null) { |
|||
DismissDirection? direction = this._dismissDirection; |
|||
if (direction == DismissDirection.endToStart || direction == DismissDirection.up) { |
|||
background = this.widget.secondaryBackground; |
|||
} |
|||
} |
|||
|
|||
if (this._resizeAnimation != null) { |
|||
// we've been dragged aside, and are now resizing.
|
|||
D.assert(() => { |
|||
if (this._resizeAnimation.status != AnimationStatus.forward) { |
|||
D.assert(this._resizeAnimation.status == AnimationStatus.completed); |
|||
throw new UIWidgetsError( |
|||
"A dismissed Dismissible widget is still part of the tree.\n" + |
|||
"Make sure to implement the onDismissed handler and to immediately remove the Dismissible\n" + |
|||
"widget from the application once that handler has fired." |
|||
); |
|||
} |
|||
|
|||
return true; |
|||
}); |
|||
|
|||
return new SizeTransition( |
|||
sizeFactor: this._resizeAnimation, |
|||
axis: this._directionIsXAxis ? Axis.vertical : Axis.horizontal, |
|||
child: new SizedBox( |
|||
width: this._sizePriorToCollapse.width, |
|||
height: this._sizePriorToCollapse.height, |
|||
child: background |
|||
) |
|||
); |
|||
} |
|||
|
|||
Widget content = new SlideTransition( |
|||
position: this._moveAnimation, |
|||
child: this.widget.child |
|||
); |
|||
|
|||
if (background != null) { |
|||
List<Widget> children = new List<Widget> { }; |
|||
|
|||
if (!this._moveAnimation.isDismissed) { |
|||
children.Add(Positioned.fill( |
|||
child: new ClipRect( |
|||
clipper: new _DismissibleClipper( |
|||
axis: this._directionIsXAxis ? Axis.horizontal : Axis.vertical, |
|||
moveAnimation: this._moveAnimation |
|||
), |
|||
child: background |
|||
) |
|||
)); |
|||
} |
|||
|
|||
children.Add(content); |
|||
content = new Stack(children: children); |
|||
} |
|||
|
|||
return new GestureDetector( |
|||
onHorizontalDragStart: this._directionIsXAxis ? (GestureDragStartCallback) this._handleDragStart : null, |
|||
onHorizontalDragUpdate: this._directionIsXAxis |
|||
? (GestureDragUpdateCallback) this._handleDragUpdate |
|||
: null, |
|||
onHorizontalDragEnd: this._directionIsXAxis ? (GestureDragEndCallback) this._handleDragEnd : null, |
|||
onVerticalDragStart: this._directionIsXAxis ? null : (GestureDragStartCallback) this._handleDragStart, |
|||
onVerticalDragUpdate: this._directionIsXAxis |
|||
? null |
|||
: (GestureDragUpdateCallback) this._handleDragUpdate, |
|||
onVerticalDragEnd: this._directionIsXAxis ? null : (GestureDragEndCallback) this._handleDragEnd, |
|||
behavior: HitTestBehavior.opaque, |
|||
child: content |
|||
); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: abd926e0982c44ca80edbb282a52b90b |
|||
timeCreated: 1552033946 |
撰写
预览
正在加载...
取消
保存
Reference in new issue