新增网络可视化请求工具

This commit is contained in:
2026-07-22 09:24:48 +08:00
parent 4ff93edaab
commit d6e80df5bf
230 changed files with 92710 additions and 51 deletions
@@ -0,0 +1,10 @@
/// This allows a value of type T or T?
/// to be treated as a value of type T?.
///
/// We use this so that APIs that have become
/// non-nullable can still be used with `!` and `?`
/// to support older versions of the API as well.
// refer to https://github.com/flutter/website/blob/main/src/development/tools/sdk/release-notes/release-notes-3.0.0.md#your-code
// TODO remove this when we no longer support before Flutter 3.0.0 and replace with following:
// SomeBinding.instance.someFunction(...);
T? bindingAmbiguate<T>(T? value) => value;
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'binding_ambiguate.dart';
const Size dotSize = Size(65.0, 65.0);
const double margin = 10.0;
const double bottomDistance = margin * 4;
const int kMaxTooltipLines = 10;
const double kScreenEdgeMargin = 10.0;
const double kTooltipPadding = 5.0;
const Color kTooltipBackgroundColor = Color.fromARGB(230, 60, 60, 60);
const Color kHighlightedRenderObjectFillColor =
Color.fromARGB(128, 128, 128, 255);
const Color kHighlightedRenderObjectBorderColor =
Color.fromARGB(128, 64, 64, 128);
const Color kTipTextColor = Color(0xFFFFFFFF);
final double ratio =
bindingAmbiguate(WidgetsBinding.instance)!.window.devicePixelRatio;
final Size windowSize =
bindingAmbiguate(WidgetsBinding.instance)!.window.physicalSize / ratio;
@@ -0,0 +1,277 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
import 'package:flutter_ume/util/constants.dart';
import 'package:tuple/tuple.dart';
import 'package:flutter_ume/util/store_mixin.dart';
typedef ToolbarAction = void Function();
class FloatingWidget extends StatefulWidget {
FloatingWidget({
Key? key,
this.contentWidget,
this.closeAction,
this.toolbarActions,
this.minimalHeight = 120,
}) : super(key: key);
final Widget? contentWidget;
final CloseAction? closeAction;
final List<Tuple3<String, Widget, ToolbarAction>>? toolbarActions;
final double minimalHeight;
@override
_FloatingWidgetState createState() => _FloatingWidgetState();
}
const double _dragBarHeight = 32;
const double _toolBarHeight = 32;
class _FloatingWidgetState extends State<FloatingWidget> with StoreMixin {
Size _windowSize = windowSize;
double _dy = 0;
bool _fullScreen = false;
double get toolBarHeight =>
(widget.toolbarActions != null && widget.toolbarActions!.isNotEmpty)
? _toolBarHeight
: 0;
@override
void initState() {
fetchWithKey('floating_widget').then((value) {
if (value != null) {
setState(() {
_dy = value;
});
}
});
_dy = _windowSize.height -
widget.minimalHeight -
_dragBarHeight -
toolBarHeight;
super.initState();
}
void _dragEvent(DragUpdateDetails details) {
_dy += details.delta.dy;
_dy = min(
max(0, _dy),
MediaQuery.of(context).size.height -
widget.minimalHeight -
_dragBarHeight -
toolBarHeight -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom);
setState(() {});
}
void _dragEnd(DragEndDetails details) async {
await storeWithKey('floating_widget', _dy);
}
@override
Widget build(BuildContext context) {
if (_windowSize.isEmpty) {
_dy =
MediaQuery.of(context).size.height - dotSize.height - bottomDistance;
_windowSize = MediaQuery.of(context).size;
}
return Container(
width: _windowSize.width,
height: _windowSize.height,
child: Stack(alignment: Alignment.center, children: <Widget>[
Positioned(
left: 0,
top: _fullScreen ? 0 : _dy,
child: _ToolBarContent(
minimalHeight: widget.minimalHeight,
contentWidget: widget.contentWidget,
dragCallback: _dragEvent,
dragEnd: _dragEnd,
maximalAction: () {
setState(() {
_fullScreen = !_fullScreen;
});
},
closeAction: widget.closeAction,
toolbarActions: widget.toolbarActions,
),
)
]));
}
}
class _ToolBarContent extends StatefulWidget {
_ToolBarContent(
{Key? key,
this.contentWidget,
this.dragCallback,
this.dragEnd,
this.maximalAction,
this.closeAction,
this.toolbarActions,
required this.minimalHeight})
: super(key: key);
final Widget? contentWidget;
final Function? dragCallback;
final Function? dragEnd;
final CloseAction? closeAction;
final MaximalAction? maximalAction;
final List<Tuple3<String, Widget, ToolbarAction>>? toolbarActions;
final double minimalHeight;
@override
__ToolBarContentState createState() => __ToolBarContentState();
}
class __ToolBarContentState extends State<_ToolBarContent> {
bool _fullScreen = false;
Size _windowSize = windowSize;
double get toolBarHeight =>
(widget.toolbarActions != null && widget.toolbarActions!.isNotEmpty)
? _toolBarHeight
: 0;
@override
Widget build(BuildContext context) {
if (_windowSize.isEmpty) {
_windowSize = MediaQuery.of(context).size;
}
const cornerRadius = Radius.circular(10);
return SafeArea(
child: Material(
borderRadius:
BorderRadius.only(topLeft: cornerRadius, topRight: cornerRadius),
elevation: 20,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: cornerRadius, topRight: cornerRadius),
color: Color(0xffd0d0d0),
),
width: MediaQuery.of(context).size.width,
height: _fullScreen
? _windowSize.height
: widget.minimalHeight + _dragBarHeight + toolBarHeight,
child: Column(
children: [
Container(
height: _dragBarHeight,
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Row(
children: [
InkWell(
onTap: () {
if (widget.closeAction != null) {
widget.closeAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: const Color(0xffff5a52),
)),
const SizedBox(
width: 8,
),
InkWell(
onTap: () {
if (widget.maximalAction != null) {
widget.maximalAction!();
}
setState(() {
_fullScreen = !_fullScreen;
});
},
child: CircleAvatar(
radius: 10,
backgroundColor: _fullScreen
? const Color(0xffe6c029)
: const Color(0xff53c22b),
)),
Expanded(
child: GestureDetector(
onVerticalDragUpdate: (details) =>
_dragCallback(details),
onVerticalDragEnd: (details) => _dragEnd(details),
child: Container(
height: _dragBarHeight,
color: const Color(0xffd0d0d0),
child: Center(
child: Text(
'UME',
style: const TextStyle(
color: Color(0xff575757),
fontSize: 14,
fontWeight: FontWeight.w600),
),
),
),
),
),
],
),
),
),
Container(
height: _fullScreen
? _windowSize.height -
_dragBarHeight -
toolBarHeight -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom
: widget.minimalHeight,
child: widget.contentWidget,
),
if (widget.toolbarActions != null &&
widget.toolbarActions!.isNotEmpty)
Container(
alignment: Alignment.centerLeft,
height: _toolBarHeight,
child: SingleChildScrollView(
// padding: const EdgeInsets.only(left: 80),
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: widget.toolbarActions!.map((tuple) {
final title = tuple.item1;
final widget = tuple.item2;
final action = tuple.item3;
return Padding(
padding: const EdgeInsets.only(left: 6, right: 6),
child: GestureDetector(
child: Container(
child: Row(
children: [
widget,
Text(title),
],
),
),
onTap: action,
),
);
}).toList(),
),
),
),
],
),
),
),
);
}
_dragCallback(DragUpdateDetails details) {
if (widget.dragCallback != null) widget.dragCallback!(details);
}
_dragEnd(DragEndDetails details) {
if (widget.dragEnd != null) widget.dragEnd!(details);
}
}
@@ -0,0 +1,632 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, TextBox, lerpDouble, Color;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'
hide FlutterLogo, FlutterLogoDecoration, FlutterLogoStyle;
/// The Flutter logo, in widget form. This widget respects the [IconTheme].
/// For guidelines on using the Flutter logo, visit https://flutter.dev/brand.
///
/// See also:
///
/// * [IconTheme], which provides ambient configuration for icons.
/// * [Icon], for showing icons the Material design icon library.
/// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s.
class FlutterLogo extends StatelessWidget {
/// Creates a widget that paints the Flutter logo.
///
/// The [size] defaults to the value given by the current [IconTheme].
const FlutterLogo({
Key? key,
this.size,
this.colors,
this.textColor = const Color(0xFF616161),
this.style = FlutterLogoStyle.markOnly,
this.duration = const Duration(milliseconds: 750),
this.curve = Curves.fastOutSlowIn,
}) : super(key: key);
/// The size of the logo in logical pixels.
///
/// The logo will be fit into a square this size.
///
/// Defaults to the current [IconTheme] size, if any. If there is no
/// [IconTheme], or it does not specify an explicit size, then it defaults to
/// 24.0.
final double? size;
/// The color swatch to use to paint the logo, [Colors.blue] by default.
///
/// If for some reason the default colors are impractical, then one
/// of [Colors.amber], [Colors.red], or [Colors.indigo] swatches can be used.
/// These are Flutter's secondary colors.
///
/// In extreme cases where none of those four color schemes will work,
/// [Colors.pink], [Colors.purple], or [Colors.cyan] swatches can be used.
/// These are Flutter's tertiary colors.
final MaterialColor? colors;
/// The color used to paint the "Flutter" text on the logo, if [style] is
/// [FlutterLogoStyle.horizontal] or [FlutterLogoStyle.stacked]. The
/// appropriate color is `const Color(0xFF616161)` (a medium gray), against a
/// white background.
final Color textColor;
/// Whether and where to draw the "Flutter" text. By default, only the logo
/// itself is drawn.
final FlutterLogoStyle style;
/// The length of time for the animation if the [style], [colors], or
/// [textColor] properties are changed.
final Duration duration;
/// The curve for the logo animation if the [style], [colors], or [textColor]
/// change.
final Curve curve;
@override
Widget build(BuildContext context) {
final IconThemeData iconTheme = IconTheme.of(context);
final double? iconSize = size ?? iconTheme.size;
final MaterialColor logoColors = colors ?? Colors.blue;
return AnimatedContainer(
width: iconSize,
height: iconSize,
duration: duration,
curve: curve,
decoration: FlutterLogoDecoration(
lightColor: logoColors.shade400,
darkColor: logoColors.shade900,
style: style,
textColor: textColor,
),
);
}
}
/// Possible ways to draw Flutter's logo.
enum FlutterLogoStyle {
/// Show only Flutter's logo, not the "Flutter" label.
///
/// This is the default behavior for [FlutterLogoDecoration] objects.
markOnly,
/// Show Flutter's logo on the left, and the "Flutter" label to its right.
horizontal,
/// Show Flutter's logo above the "Flutter" label.
stacked,
}
/// An immutable description of how to paint Flutter's logo.
class FlutterLogoDecoration extends Decoration {
/// Creates a decoration that knows how to paint Flutter's logo.
///
/// The [lightColor] and [darkColor] are used to fill the logo. The [style]
/// controls whether and where to draw the "Flutter" label. If one is shown,
/// the [textColor] controls the color of the label.
///
/// The [lightColor], [darkColor], [textColor], [style], and [margin]
/// arguments must not be null.
const FlutterLogoDecoration({
ui.Color this.lightColor = const Color(0xFF42A5F5), // Colors.blue[400]
ui.Color this.darkColor = const Color(0xFF0D47A1), // Colors.blue[900]
ui.Color this.textColor = const Color(0xFF616161),
this.style = FlutterLogoStyle.markOnly,
EdgeInsets this.margin = EdgeInsets.zero,
}) : _position = identical(style, FlutterLogoStyle.markOnly)
? 0.0
: identical(style, FlutterLogoStyle.horizontal)
? 1.0
: -1.0,
// (see https://github.com/dart-lang/sdk/issues/26980 for details about that ignore statement)
_opacity = 1.0;
const FlutterLogoDecoration._(this.lightColor, this.darkColor, this.textColor,
this.style, this.margin, this._position, this._opacity);
/// The lighter of the two colors used to paint the logo.
///
/// If possible, the default should be used. It corresponds to the 400 and 900
/// values of [material.Colors.blue] from the Material library.
///
/// If for some reason that color scheme is impractical, the same entries from
/// [material.Colors.amber], [material.Colors.red], or
/// [material.Colors.indigo] colors can be used. These are Flutter's secondary
/// colors.
///
/// In extreme cases where none of those four color schemes will work,
/// [material.Colors.pink], [material.Colors.purple], or
/// [material.Colors.cyan] can be used. These are Flutter's tertiary colors.
final Color? lightColor;
/// The darker of the two colors used to paint the logo.
///
/// See [lightColor] for more information about selecting the logo's colors.
final Color? darkColor;
/// The color used to paint the "Flutter" text on the logo, if [style] is
/// [FlutterLogoStyle.horizontal] or [FlutterLogoStyle.stacked]. The
/// appropriate color is `const Color(0xFF616161)` (a medium gray), against a
/// white background.
final Color? textColor;
/// Whether and where to draw the "Flutter" text. By default, only the logo
/// itself is drawn.
// This property isn't actually used when painting. It's only really used to
// set the internal _position property.
final FlutterLogoStyle style;
/// How far to inset the logo from the edge of the container.
final EdgeInsets? margin;
// The following are set when lerping, to represent states that can't be
// represented by the constructor.
final double
_position; // -1.0 for stacked, 1.0 for horizontal, 0.0 for no logo
final double _opacity; // 0.0 .. 1.0
bool get _inTransition =>
_opacity != 1.0 ||
(_position != -1.0 && _position != 0.0 && _position != 1.0);
@override
bool debugAssertIsValid() {
assert(lightColor != null &&
darkColor != null &&
textColor != null &&
margin != null &&
_position.isFinite &&
_opacity >= 0.0 &&
_opacity <= 1.0);
return true;
}
@override
bool get isComplex => !_inTransition;
/// Linearly interpolate between two Flutter logo descriptions.
///
/// Interpolates both the color and the style in a continuous fashion.
///
/// If both values are null, this returns null. Otherwise, it returns a
/// non-null value. If one of the values is null, then the result is obtained
/// by scaling the other value's opacity and [margin].
///
/// {@macro dart.ui.shadow.lerp}
///
/// See also:
///
/// * [Decoration.lerp], which interpolates between arbitrary decorations.
static FlutterLogoDecoration? lerp(
FlutterLogoDecoration? a, FlutterLogoDecoration? b, double t) {
assert(a == null || a.debugAssertIsValid());
assert(b == null || b.debugAssertIsValid());
if (a == null && b == null) return null;
if (a == null) {
return FlutterLogoDecoration._(
b!.lightColor,
b.darkColor,
b.textColor,
b.style,
b.margin! * t,
b._position,
b._opacity * t.clamp(0.0, 1.0),
);
}
if (b == null) {
return FlutterLogoDecoration._(
a.lightColor,
a.darkColor,
a.textColor,
a.style,
a.margin! * t,
a._position,
a._opacity * (1.0 - t).clamp(0.0, 1.0),
);
}
if (t == 0.0) return a;
if (t == 1.0) return b;
return FlutterLogoDecoration._(
Color.lerp(a.lightColor, b.lightColor, t),
Color.lerp(a.darkColor, b.darkColor, t),
Color.lerp(a.textColor, b.textColor, t),
t < 0.5 ? a.style : b.style,
EdgeInsets.lerp(a.margin, b.margin, t),
a._position + (b._position - a._position) * t,
(a._opacity + (b._opacity - a._opacity) * t).clamp(0.0, 1.0),
);
}
@override
FlutterLogoDecoration? lerpFrom(Decoration? a, double t) {
assert(debugAssertIsValid());
if (a == null || a is FlutterLogoDecoration) {
assert(a == null || a.debugAssertIsValid());
return FlutterLogoDecoration.lerp(a as FlutterLogoDecoration?, this, t);
}
return super.lerpFrom(a, t) as FlutterLogoDecoration?;
}
@override
FlutterLogoDecoration? lerpTo(Decoration? b, double t) {
assert(debugAssertIsValid());
if (b == null || b is FlutterLogoDecoration) {
assert(b == null || b.debugAssertIsValid());
return FlutterLogoDecoration.lerp(this, b as FlutterLogoDecoration?, t);
}
return super.lerpTo(b, t) as FlutterLogoDecoration?;
}
@override
// ignore: todo
// TODO(ianh): better hit testing
bool hitTest(Size size, Offset position, {TextDirection? textDirection}) =>
true;
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
assert(debugAssertIsValid());
return _FlutterLogoPainter(this);
}
@override
bool operator ==(dynamic other) {
assert(debugAssertIsValid());
if (identical(this, other)) return true;
if (other is! FlutterLogoDecoration) return false;
final FlutterLogoDecoration typedOther = other;
return lightColor == typedOther.lightColor &&
darkColor == typedOther.darkColor &&
textColor == typedOther.textColor &&
_position == typedOther._position &&
_opacity == typedOther._opacity;
}
@override
int get hashCode {
assert(debugAssertIsValid());
return Object.hash(
lightColor,
darkColor,
textColor,
_position,
_opacity,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(DiagnosticsNode.message('$lightColor/$darkColor on $textColor'));
properties.add(EnumProperty<FlutterLogoStyle>('style', style));
if (_inTransition) {
properties.add(DiagnosticsNode.message(
'transition ${debugFormatDouble(_position)}:${debugFormatDouble(_opacity)}'));
}
}
}
/// An object that paints a [BoxDecoration] into a canvas.
class _FlutterLogoPainter extends BoxPainter {
_FlutterLogoPainter(this._config)
: assert(_config.debugAssertIsValid()),
super(null) {
_prepareText();
}
final FlutterLogoDecoration _config;
// these are configured assuming a font size of 100.0.
late TextPainter _textPainter;
Rect? _textBoundingRect;
void _prepareText() {
const String kLabel = 'Flutter';
_textPainter = TextPainter(
text: TextSpan(
text: kLabel,
style: TextStyle(
color: _config.textColor,
fontFamily: 'Roboto',
fontSize: 100.0 *
350.0 /
247.0, // 247 is the height of the F when the fontSize is 350, assuming device pixel ratio 1.0
fontWeight: FontWeight.w300,
textBaseline: TextBaseline.alphabetic,
),
),
textDirection: TextDirection.ltr,
);
_textPainter.layout();
final ui.TextBox textSize = _textPainter
.getBoxesForSelection(
const TextSelection(baseOffset: 0, extentOffset: kLabel.length))
.single;
_textBoundingRect = Rect.fromLTRB(
textSize.left, textSize.top, textSize.right, textSize.bottom);
}
// This class contains a lot of magic numbers. They were derived from the
// values in the SVG files exported from the original artwork source.
void _paintLogo(Canvas canvas, Rect rect) {
// Our points are in a coordinate space that's 166 pixels wide and 202 pixels high.
// First, transform the rectangle so that our coordinate space is a square 202 pixels
// to a side, with the top left at the origin.
canvas.save();
canvas.translate(rect.left, rect.top);
canvas.scale(rect.width / 202.0, rect.height / 202.0);
// Next, offset it some more so that the 166 horizontal pixels are centered
// in that square (as opposed to being on the left side of it). This means
// that if we draw in the rectangle from 0,0 to 166,202, we are drawing in
// the center of the given rect.
canvas.translate((202.0 - 166.0) / 2.0, 0.0);
// Set up the styles.
final Paint lightPaint = Paint()
..color = _config.lightColor!.withOpacity(0.8);
final Paint mediumPaint = Paint()..color = _config.lightColor!;
final Paint darkPaint = Paint()..color = _config.darkColor!;
final ui.Gradient triangleGradient = ui.Gradient.linear(
const Offset(87.2623 + 37.9092, 28.8384 + 123.4389),
const Offset(42.9205 + 37.9092, 35.0952 + 123.4389),
<Color>[
const Color(0xBFFFFFFF),
const Color(0xBFFCFCFC),
const Color(0xBFF4F4F4),
const Color(0xBFE5E5E5),
const Color(0xBFD1D1D1),
const Color(0xBFB6B6B6),
const Color(0xBF959595),
const Color(0xBF6E6E6E),
const Color(0xBF616161),
],
<double>[
0.2690,
0.4093,
0.4972,
0.5708,
0.6364,
0.6968,
0.7533,
0.8058,
0.8219
],
);
final Paint trianglePaint = Paint()
..shader = triangleGradient
..blendMode = BlendMode.multiply;
final ui.Gradient rectangleGradient = ui.Gradient.linear(
const Offset(62.3643 + 37.9092, 40.135 + 123.4389),
const Offset(54.0376 + 37.9092, 31.8083 + 123.4389),
<Color>[
const Color(0x80FFFFFF),
const Color(0x80FCFCFC),
const Color(0x80F4F4F4),
const Color(0x80E5E5E5),
const Color(0x80D1D1D1),
const Color(0x80B6B6B6),
const Color(0x80959595),
const Color(0x806E6E6E),
const Color(0x80616161),
],
<double>[
0.4588,
0.5509,
0.6087,
0.6570,
0.7001,
0.7397,
0.7768,
0.8113,
0.8219
],
);
final Paint rectanglePaint = Paint()
..shader = rectangleGradient
..blendMode = BlendMode.multiply;
// Draw the basic shape.
final Path topBeam = Path()
..moveTo(37.7, 128.9)
..lineTo(9.8, 101.0)
..lineTo(100.4, 10.4)
..lineTo(156.2, 10.4);
canvas.drawPath(topBeam, lightPaint);
final Path middleBeam = Path()
..moveTo(156.2, 94.0)
..lineTo(100.4, 94.0)
..lineTo(79.5, 114.9)
..lineTo(107.4, 142.8);
canvas.drawPath(middleBeam, lightPaint);
final Path bottomBeam = Path()
..moveTo(79.5, 170.7)
..lineTo(100.4, 191.6)
..lineTo(156.2, 191.6)
..lineTo(156.2, 191.6)
..lineTo(107.4, 142.8);
canvas.drawPath(bottomBeam, darkPaint);
canvas.save();
canvas.transform(Float64List.fromList(const <double>[
// careful, this is in _column_-major order
0.7071, -0.7071, 0.0, 0.0,
0.7071, 0.7071, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
-77.697, 98.057, 0.0, 1.0,
]));
canvas.drawRect(const Rect.fromLTWH(59.8, 123.1, 39.4, 39.4), mediumPaint);
canvas.restore();
// The two gradients.
final Path triangle = Path()
..moveTo(79.5, 170.7)
..lineTo(120.9, 156.4)
..lineTo(107.4, 142.8);
canvas.drawPath(triangle, trianglePaint);
final Path rectangle = Path()
..moveTo(107.4, 142.8)
..lineTo(79.5, 170.7)
..lineTo(86.1, 177.3)
..lineTo(114.0, 149.4);
canvas.drawPath(rectangle, rectanglePaint);
canvas.restore();
}
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
offset += _config.margin!.topLeft;
final Size canvasSize = _config.margin!.deflateSize(configuration.size!);
if (canvasSize.isEmpty) return;
Size logoSize;
if (_config._position > 0.0) {
// horizontal style
logoSize = const Size(820.0, 232.0);
} else if (_config._position < 0.0) {
// stacked style
logoSize = const Size(252.0, 306.0);
} else {
// only the mark
logoSize = const Size(202.0, 202.0);
}
final FittedSizes fittedSize =
applyBoxFit(BoxFit.contain, logoSize, canvasSize);
assert(fittedSize.source == logoSize);
final Rect rect =
Alignment.center.inscribe(fittedSize.destination, offset & canvasSize);
final double centerSquareHeight = canvasSize.shortestSide;
final Rect centerSquare = Rect.fromLTWH(
offset.dx + (canvasSize.width - centerSquareHeight) / 2.0,
offset.dy + (canvasSize.height - centerSquareHeight) / 2.0,
centerSquareHeight,
centerSquareHeight,
);
Rect logoTargetSquare;
if (_config._position > 0.0) {
// horizontal style
logoTargetSquare =
Rect.fromLTWH(rect.left, rect.top, rect.height, rect.height);
} else if (_config._position < 0.0) {
// stacked style
final double logoHeight = rect.height * 191.0 / 306.0;
logoTargetSquare = Rect.fromLTWH(
rect.left + (rect.width - logoHeight) / 2.0,
rect.top,
logoHeight,
logoHeight,
);
} else {
// only the mark
logoTargetSquare = centerSquare;
}
final Rect logoSquare =
Rect.lerp(centerSquare, logoTargetSquare, _config._position.abs())!;
if (_config._opacity < 1.0) {
canvas.saveLayer(
offset & canvasSize,
Paint()
..colorFilter = ColorFilter.mode(
const Color(0xFFFFFFFF).withOpacity(_config._opacity),
BlendMode.modulate,
),
);
}
if (_config._position != 0.0) {
if (_config._position > 0.0) {
// horizontal style
final double fontSize =
2.0 / 3.0 * logoSquare.height * (1 - (10.4 * 2.0) / 202.0);
final double scale = fontSize / 100.0;
final double
finalLeftTextPosition = // position of text in rest position
(256.4 / 820.0) *
rect
.width - // 256.4 is the distance from the left edge to the left of the F when the whole logo is 820.0 wide
(32.0 / 350.0) *
fontSize; // 32 is the distance from the text bounding box edge to the left edge of the F when the font size is 350
final double
initialLeftTextPosition = // position of text when just starting the animation
rect.width / 2.0 - _textBoundingRect!.width * scale;
final Offset textOffset = Offset(
rect.left +
ui.lerpDouble(initialLeftTextPosition, finalLeftTextPosition,
_config._position)!,
rect.top + (rect.height - _textBoundingRect!.height * scale) / 2.0,
);
canvas.save();
if (_config._position < 1.0) {
final Offset center = logoSquare.center;
final Path path = Path()
..moveTo(center.dx, center.dy)
..lineTo(center.dx + rect.width, center.dy - rect.width)
..lineTo(center.dx + rect.width, center.dy + rect.width)
..close();
canvas.clipPath(path);
}
canvas.translate(textOffset.dx, textOffset.dy);
canvas.scale(scale, scale);
_textPainter.paint(canvas, Offset.zero);
canvas.restore();
} else if (_config._position < 0.0) {
// stacked style
final double fontSize =
0.35 * logoTargetSquare.height * (1 - (10.4 * 2.0) / 202.0);
final double scale = fontSize / 100.0;
if (_config._position > -1.0) {
// This limits what the drawRect call below is going to blend with.
canvas.saveLayer(_textBoundingRect, Paint());
} else {
canvas.save();
}
canvas.translate(
logoTargetSquare.center.dx - (_textBoundingRect!.width * scale / 2.0),
logoTargetSquare.bottom,
);
canvas.scale(scale, scale);
_textPainter.paint(canvas, Offset.zero);
if (_config._position > -1.0) {
canvas.drawRect(
_textBoundingRect!.inflate(_textBoundingRect!.width * 0.5),
Paint()
..blendMode = BlendMode.modulate
..shader = ui.Gradient.linear(
Offset(_textBoundingRect!.width * -0.5, 0.0),
Offset(_textBoundingRect!.width * 1.5, 0.0),
<Color>[
const Color(0xFFFFFFFF),
const Color(0xFFFFFFFF),
const Color(0x00FFFFFF),
const Color(0x00FFFFFF)
],
<double>[
0.0,
math.max(0.0, _config._position.abs() - 0.1),
math.min(_config._position.abs() + 0.1, 1.0),
1.0
],
),
);
}
canvas.restore();
}
}
_paintLogo(canvas, logoSquare);
if (_config._opacity < 1.0) canvas.restore();
}
}
@@ -0,0 +1,32 @@
import 'dart:core';
extension JsonListPathWriter on List {
List write(String path, dynamic value) {
dynamic obj = this;
dynamic root = this;
final elements = path.split('.');
for (final element in elements.sublist(0, elements.length - 1)) {
if (element == r'$') continue;
if (RegExp(r'\[[0-9]+\]').hasMatch(element)) {
final index = int.parse(RegExp(r'\d+').stringMatch(element)!);
obj = obj[index];
continue;
}
if (RegExp(r'\["[0-9a-zA-Z]+"]').hasMatch(element)) {
final key = RegExp(r'[0-9a-zA-Z]+').stringMatch(element);
obj = obj[key];
continue;
}
}
final lastKey = elements.last;
if (RegExp(r'\[[0-9]+\]').hasMatch(lastKey)) {
final index = int.parse(RegExp(r'\d+').stringMatch(lastKey)!);
obj[index] = value;
} else if (RegExp(r'\["[0-9a-zA-Z]+"]').hasMatch(lastKey)) {
final key = RegExp(r'[0-9a-zA-Z]+').stringMatch(lastKey);
obj[key] = value;
}
return root;
}
}
@@ -0,0 +1,32 @@
import 'dart:core';
extension JsonMapPathWriter on Map {
Map write(String path, dynamic value) {
dynamic obj = this;
dynamic root = this;
final elements = path.split('.');
for (final element in elements.sublist(0, elements.length - 1)) {
if (element == r'$') continue;
if (RegExp(r'\[[0-9]+\]').hasMatch(element)) {
final index = int.parse(RegExp(r'\d+').stringMatch(element)!);
obj = obj[index];
continue;
}
if (RegExp(r'\["[0-9a-zA-Z]+"]').hasMatch(element)) {
final key = RegExp(r'[0-9a-zA-Z]+').stringMatch(element);
obj = obj[key];
continue;
}
}
final lastKey = elements.last;
if (RegExp(r'\[[0-9]+\]').hasMatch(lastKey)) {
final index = int.parse(RegExp(r'\d+').stringMatch(lastKey)!);
obj[index] = value;
} else if (RegExp(r'\["[0-9a-zA-Z]+"]').hasMatch(lastKey)) {
final key = RegExp(r'[0-9a-zA-Z]+').stringMatch(lastKey);
obj[key] = value;
}
return root;
}
}
@@ -0,0 +1,32 @@
import 'package:shared_preferences/shared_preferences.dart';
mixin StoreMixin {
Future<SharedPreferences> _sharedPref = SharedPreferences.getInstance();
/// Store a object with the [key]. The [obj] must be one of [bool], [double], [int], [String] or [List]<String>.
Future<void> storeWithKey(String key, dynamic obj) async {
if (obj == null) {
return;
}
final savedKey = 'ume_${runtimeType.toString}_$key';
final SharedPreferences prefs = await _sharedPref;
if (obj is bool) {
await prefs.setBool(savedKey, obj);
} else if (obj is double) {
await prefs.setDouble(savedKey, obj);
} else if (obj is int) {
await prefs.setInt(savedKey, obj);
} else if (obj is String) {
await prefs.setString(savedKey, obj);
} else if (obj is List<String>) {
await prefs.setStringList(savedKey, obj);
}
}
/// Fetch a object by the [key]
Future<dynamic> fetchWithKey(String key) async {
final SharedPreferences prefs = await _sharedPref;
final savedKey = 'ume_${runtimeType.toString}_$key';
return prefs.get(savedKey);
}
}