新增网络可视化请求工具

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,30 @@
import 'package:flutter/widgets.dart';
import 'package:tuple/tuple.dart';
abstract class Pluggable {
String get name;
String get displayName;
void onTrigger();
Widget? buildWidget(BuildContext? context);
ImageProvider get iconImageProvider;
}
typedef StreamFilter = bool Function(dynamic);
abstract class PluggableWithStream extends Pluggable {
Stream get stream;
StreamFilter get streamFilter;
}
abstract class PluggableWithNestedWidget extends Pluggable {
Widget buildNestedWidget(Widget child);
}
abstract class PluggableWithAnywhereDoor extends Pluggable {
NavigatorState? get navigator;
Tuple2<String, Object?>? get routeNameAndArgs;
Route? get route;
void popResultReceive(dynamic result);
}
@@ -0,0 +1,99 @@
import 'dart:async';
import 'package:flutter_ume/core/plugin_manager.dart';
import 'package:flutter_ume/core/pluggable.dart';
class PluggableMessageService {
static final PluggableMessageService _instance =
PluggableMessageService._internal();
factory PluggableMessageService() {
return _instance;
}
// ignore: close_sinks
StreamController<PluggableMessage> messageStreamController =
StreamController.broadcast();
Map<String, PluggableMessageInfo> get pluggableMessageData =>
_pluggableMessageData;
Map<String, PluggableMessageInfo> _pluggableMessageData = Map();
PluggableMessageService._internal() {
_pluggableMessageData = Map();
}
void resetListener() {
clearListener();
PluginManager.instance.pluginsMap.values
.whereType<PluggableWithStream>()
.forEach((element) {
final pluggable = element;
// ignore: cancel_subscriptions
final subscription = pluggable.stream.where((event) {
return pluggable.streamFilter(event);
}).listen((event) {
_pluggableMessageData[pluggable.name]!.increaseCounter();
_sendSink(pluggable);
});
_pluggableMessageData.update(pluggable.name, (old) {
old.subscription?.cancel();
return PluggableMessageInfo.subscription(subscription);
}, ifAbsent: () => PluggableMessageInfo.subscription(subscription));
});
}
int countAll(List<Pluggable?> pluggable) {
int result = 0;
pluggable.map((e) => e!.name).toSet().forEach((element) {
result += (_pluggableMessageData[element]?.count ?? 0);
});
return result;
}
int count(Pluggable pluggable) =>
_pluggableMessageData[pluggable.name]?.count ?? 0;
void resetCounter(Pluggable pluggable) {
_pluggableMessageData[pluggable.name]?.resetCounter();
_sendSink(pluggable);
}
void clearListener() {
_pluggableMessageData.values.forEach((messageInfo) {
messageInfo.subscription?.cancel();
});
_pluggableMessageData.clear();
}
void _sendSink(Pluggable pluggable) {
messageStreamController.sink.add(PluggableMessage.create(
pluggable.name, PluggableMessageService().count(pluggable)));
}
}
class PluggableMessageInfo {
StreamSubscription<dynamic>? _subscription;
int _count = 0;
StreamSubscription<dynamic>? get subscription => _subscription;
int get count => _count;
PluggableMessageInfo.subscription(StreamSubscription<dynamic> subscription) {
_subscription = subscription;
_count = 0;
}
void increaseCounter() {
_count++;
}
void resetCounter() {
_count = 0;
}
}
class PluggableMessage {
int _count;
String _key;
int get count => _count;
String get key => _key;
PluggableMessage.create(this._key, this._count);
}
@@ -0,0 +1,47 @@
import 'package:flutter_ume/flutter_ume.dart';
import 'package:flutter_ume/core/pluggable.dart';
class PluginManager {
static PluginManager? _instance;
Map<String, Pluggable?> get pluginsMap => _pluginsMap;
Map<String, Pluggable?> _pluginsMap = {};
Pluggable? _activatedPluggable;
String? get activatedPluggableName => _activatedPluggable?.name;
static PluginManager get instance {
if (_instance == null) {
_instance = PluginManager._();
}
return _instance!;
}
PluginManager._();
/// Register a single [plugin]
void register(Pluggable plugin) {
if (plugin.name.isEmpty) {
return;
}
_pluginsMap[plugin.name] = plugin;
}
/// Register multiple [plugins]
void registerAll(List<Pluggable> plugins) {
for (final plugin in plugins) {
register(plugin);
}
}
void activatePluggable(Pluggable pluggable) {
_activatedPluggable = pluggable;
}
void deactivatePluggable(Pluggable pluggable) {
if (_activatedPluggable?.name == pluggable.name) {
_activatedPluggable = null;
}
}
}
@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/pluggable.dart';
class RedDot extends StatefulWidget {
RedDot({Key? key, required this.pluginDatas, this.size = 16})
: super(key: key);
final List<Pluggable?> pluginDatas;
final double size;
@override
_RedDotState createState() => _RedDotState();
}
class _RedDotState extends State<RedDot> {
int _count = 0;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_subscription =
PluggableMessageService().messageStreamController.stream.listen((data) {
if (mounted &&
widget.pluginDatas.any((element) => element!.name == data.key)) {
_refresh();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_refresh();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
_refresh() {
setState(() {
_count = PluggableMessageService().countAll(widget.pluginDatas);
});
}
@override
Widget build(BuildContext context) {
if (_count == 0) {
return Container();
}
return Container(
height: widget.size,
child: Padding(
padding: EdgeInsets.only(
left: widget.size * 0.28, right: widget.size * 0.28),
child: Directionality(
textDirection: TextDirection.ltr,
child: Text(
_count.toString(),
style: TextStyle(color: Colors.white, fontSize: widget.size * 0.8),
textAlign: TextAlign.center,
),
),
),
decoration: ShapeDecoration(
color: Colors.red,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(widget.size * 0.5)))),
);
}
}
@@ -0,0 +1,42 @@
import 'package:shared_preferences/shared_preferences.dart';
class PluginStoreManager {
final String _pluginStoreKey = 'PluginStoreKey';
final String _minimalToolbarSwitch = 'MinimalToolbarSwitch';
final String _floatingDotPos = 'FloatingDotPos';
Future<SharedPreferences> _sharedPref = SharedPreferences.getInstance();
Future<List<String>?> fetchStorePlugins() async {
final SharedPreferences prefs = await _sharedPref;
return prefs.getStringList(_pluginStoreKey);
}
void storePlugins(List<String> plugins) async {
if (plugins.isEmpty) {
return;
}
final SharedPreferences prefs = await _sharedPref;
await prefs.setStringList(_pluginStoreKey, plugins);
}
Future<bool?> fetchMinimalToolbarSwitch() async {
final SharedPreferences prefs = await _sharedPref;
return prefs.getBool(_minimalToolbarSwitch);
}
void storeMinimalToolbarSwitch(bool value) async {
final SharedPreferences prefs = await _sharedPref;
await prefs.setBool(_minimalToolbarSwitch, value);
}
Future<String?> fetchFloatingDotPos() async {
final SharedPreferences prefs = await _sharedPref;
return prefs.getString(_floatingDotPos);
}
void storeFloatingDotPos(double x, double y) async {
final SharedPreferences prefs = await _sharedPref;
prefs.setString(_floatingDotPos, "$x,$y");
}
}
@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
typedef CanAccept = bool Function(int oldIndex, int newIndex);
typedef DragCompletion<T> = void Function(List<T?>? data);
typedef DataWidgetBuilder<T> = Widget Function(BuildContext context, T data);
class DragableGridView<T> extends StatefulWidget {
final DataWidgetBuilder<T> itemBuilder;
final CanAccept canAccept;
final List<T> dataList;
final int crossAxisCount;
final Axis scrollDirection;
final double childAspectRatio;
final DragCompletion? dragCompletion;
DragableGridView(
this.dataList, {
Key? key,
this.scrollDirection = Axis.vertical,
this.crossAxisCount = 3,
this.childAspectRatio = 1.0,
this.dragCompletion,
required this.itemBuilder,
required this.canAccept,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _DragableGridViewState<T>();
}
class _DragableGridViewState<T> extends State<DragableGridView> {
List<T?>? dataList;
late List<T?> dataListBackup;
bool showItemWhenCovered = false;
int willAcceptIndex = -1;
int draggingItemIndex = -1;
@override
void initState() {
super.initState();
dataList = widget.dataList as List<T?>?;
dataListBackup = dataList!.sublist(0);
}
@override
Widget build(BuildContext context) {
return GridView.builder(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: dataList!.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, childAspectRatio: 0.85),
itemBuilder: ((ctx, index) {
return _buildDraggable(ctx, index);
}));
}
Widget _buildDraggable(BuildContext context, int index) {
return LayoutBuilder(
builder: (context, constraint) {
return LongPressDraggable(
data: index,
child: DragTarget<int>(
onAccept: (_) {},
builder: (context, data, rejects) {
return willAcceptIndex >= 0 && willAcceptIndex == index
? Container()
: widget.itemBuilder(context, dataList![index]);
},
onLeave: (_) {
willAcceptIndex = -1;
setState(() {
showItemWhenCovered = false;
dataList = dataListBackup.sublist(0);
});
},
onWillAccept: (int? fromIndex) {
final accept = fromIndex != index;
if (accept) {
willAcceptIndex = index;
showItemWhenCovered = true;
dataList = dataListBackup.sublist(0);
final fromData = dataList![fromIndex!];
setState(() {
dataList!.removeAt(fromIndex);
dataList!.insert(index, fromData);
});
}
return accept;
},
),
onDragStarted: () {
draggingItemIndex = index;
dataListBackup = dataList!.sublist(0);
},
onDraggableCanceled: (Velocity velocity, Offset offset) {
setState(() {
willAcceptIndex = -1;
showItemWhenCovered = false;
dataList = dataListBackup.sublist(0);
});
},
onDragCompleted: () {
if (widget.dragCompletion != null) {
widget.dragCompletion!(dataList);
}
setState(() {
showItemWhenCovered = false;
willAcceptIndex = -1;
});
},
feedback: Container(
child: SizedBox(
width: constraint.maxWidth,
height: constraint.maxHeight,
child: widget.itemBuilder(context, dataList![index]),
),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 18,
spreadRadius: 0.8,
color: Colors.black87,
),
],
borderRadius: BorderRadius.circular(6),
),
),
childWhenDragging: Container(
child: SizedBox(
child: showItemWhenCovered
? widget.itemBuilder(context, dataList![index])
: null,
),
),
);
},
);
}
}
@@ -0,0 +1,6 @@
import 'package:flutter/widgets.dart';
final GlobalKey rootKey = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'ume_navigator');
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/pluggable.dart';
class IconCache {
static Map<String, Widget> _icons = Map();
static Widget? icon({
required Pluggable pluggableInfo,
}) {
if (!_icons.containsKey(pluggableInfo.name)) {
final i = Image(image: pluggableInfo.iconImageProvider);
_icons.putIfAbsent(pluggableInfo.name, () => i);
} else if (!_icons.containsKey(pluggableInfo.name)) {
return Container();
}
return _icons[pluggableInfo.name];
}
}
@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/ui/icon_cache.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/red_dot.dart';
import 'package:flutter_ume/core/store_manager.dart';
import 'package:flutter_ume/flutter_ume.dart';
import 'dragable_widget.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
class MenuPage extends StatefulWidget {
MenuPage({Key? key, this.action, this.minimalAction, this.closeAction})
: super(key: key);
final MenuAction? action;
final MinimalAction? minimalAction;
final CloseAction? closeAction;
@override
_MenuPageState createState() => _MenuPageState();
}
class _MenuPageState extends State<MenuPage>
with SingleTickerProviderStateMixin {
PluginStoreManager _storeManager = PluginStoreManager();
List<Pluggable?> _dataList = [];
@override
void initState() {
super.initState();
_handleData();
}
void _handleData() async {
List<Pluggable?> dataList = [];
List<String>? list = await _storeManager.fetchStorePlugins();
if (list == null || list.isEmpty) {
dataList = PluginManager.instance.pluginsMap.values.toList();
} else {
list.forEach((f) {
bool contain = PluginManager.instance.pluginsMap.containsKey(f);
if (contain) {
dataList.add(PluginManager.instance.pluginsMap[f]);
}
});
PluginManager.instance.pluginsMap.keys.forEach((key) {
if (!list.contains(key)) {
dataList.add(PluginManager.instance.pluginsMap[key]);
}
});
}
_saveData(dataList);
setState(() {
_dataList = dataList;
});
}
void _saveData(List<Pluggable?> data) {
List l = data.map((f) => f!.name).toList();
if (l.isEmpty) {
return;
}
Future.delayed(Duration(milliseconds: 500), () {
_storeManager.storePlugins(l as List<String>);
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
bottom: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Colors.white,
height: 100,
width: MediaQuery.of(context).size.width,
alignment: Alignment.bottomLeft,
padding: const EdgeInsets.only(left: 16, right: 16),
margin: const EdgeInsets.only(bottom: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
InkWell(
onTap: () {
if (widget.closeAction != null) {
widget.closeAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: Color(0xffff5a52),
)),
const SizedBox(
width: 8,
),
InkWell(
onTap: () {
if (widget.minimalAction != null) {
widget.minimalAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: Color(0xffe6c029),
)),
],
),
Container(
child: Text('UME',
style: const TextStyle(
fontSize: 60,
fontWeight: FontWeight.w800,
color: Color(0xff454545)))),
],
),
),
Expanded(
child: _dataList.isEmpty
? _EmptyPlaceholder()
: DragableGridView(
_dataList,
childAspectRatio: 0.85,
canAccept: (oldIndex, newIndex) {
return true;
},
dragCompletion: (dataList) {
_saveData(dataList as List<Pluggable?>);
},
itemBuilder: (context, dynamic data) {
return GestureDetector(
onTap: () {
widget.action!(data);
PluggableMessageService().resetCounter(data);
},
behavior: HitTestBehavior.opaque,
child: _MenuCell(pluginData: data),
);
},
))
],
),
),
);
}
}
class _EmptyPlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text('Empty'),
);
}
}
class _MenuCell extends StatelessWidget {
const _MenuCell({Key? key, this.pluginData}) : super(key: key);
final Pluggable? pluginData;
@override
Widget build(BuildContext context) {
final Color lineColor = Colors.grey.withOpacity(0.25);
return LayoutBuilder(builder: (_, constraints) {
return Material(
color: Colors.white,
child: Container(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
left: 0,
top: 0,
child: Container(
height: constraints.maxHeight,
width: 0.5,
color: lineColor)),
Positioned(
left: 0,
top: 0,
child: Container(
height: 0.5,
width: constraints.maxWidth,
color: lineColor)),
Positioned(
top: 0,
right: 0,
child: Container(
height: constraints.maxHeight,
width: 0.5,
color: lineColor)),
Positioned(
bottom: 0,
left: 0,
child: Container(
height: 0.5,
width: constraints.maxWidth,
color: lineColor)),
Container(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: IconCache.icon(pluggableInfo: pluginData!),
height: 40,
width: 40),
Container(
margin: const EdgeInsets.only(top: 25),
child: Text(pluginData!.displayName,
style: const TextStyle(
fontSize: 15, color: Colors.black)))
],
),
),
Positioned(
right: 12,
top: 12,
child: RedDot(
pluginDatas: [pluginData],
size: 22,
),
),
],
),
),
);
});
}
}
@@ -0,0 +1,6 @@
import 'package:flutter_ume/core/pluggable.dart';
typedef MenuAction = void Function(Pluggable?);
typedef MinimalAction = void Function();
typedef MaximalAction = void Function();
typedef CloseAction = void Function();
@@ -0,0 +1,437 @@
import 'package:flutter/material.dart'
hide FlutterLogo, FlutterLogoDecoration, FlutterLogoStyle;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_ume/core/pluggable.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/plugin_manager.dart';
import 'package:flutter_ume/core/red_dot.dart';
import 'package:flutter_ume/core/store_manager.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
import 'package:flutter_ume/core/ui/toolbar_widget.dart';
import 'package:flutter_ume/util/binding_ambiguate.dart';
import 'package:flutter_ume/util/constants.dart';
import './menu_page.dart';
import 'global.dart';
const defaultLocalizationsDelegates = const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
];
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
/// Wrap your App widget. If [enable] is false, the function will return [child].
class UMEWidget extends StatefulWidget {
const UMEWidget({
Key? key,
required this.child,
this.enable = true,
this.supportedLocales,
this.localizationsDelegates = defaultLocalizationsDelegates,
}) : super(key: key);
final Widget child;
final bool enable;
final Iterable<Locale>? supportedLocales;
final Iterable<LocalizationsDelegate> localizationsDelegates;
/// Close the activated plugin if any.
///
/// The method does not have side-effects whether the [UMEWidget]
/// is not enabled or no plugin has been activated.
static void closeActivatedPlugin() {
final _ContentPageState? state =
_umeWidgetState?._contentPageKey.currentState;
if (state?._currentSelected != null) {
state?._closeActivatedPluggable();
}
}
@override
_UMEWidgetState createState() => _UMEWidgetState();
}
/// Hold the [_UMEWidgetState] as a global variable.
_UMEWidgetState? _umeWidgetState;
class _UMEWidgetState extends State<UMEWidget> {
_UMEWidgetState() {
// Make sure only a single `UMEWidget` is being used.
assert(
_umeWidgetState == null,
'Only one `UMEWidget` can be used at the same time.',
);
if (_umeWidgetState != null) {
throw StateError('Only one `UMEWidget` can be used at the same time.');
}
_umeWidgetState = this;
}
final GlobalKey<_ContentPageState> _contentPageKey = GlobalKey();
late Widget _child;
VoidCallback? _onMetricsChanged;
bool _overlayEntryInserted = false;
OverlayEntry _overlayEntry = OverlayEntry(
builder: (_) => const SizedBox.shrink(),
);
@override
void initState() {
super.initState();
_replaceChild();
_injectOverlay();
_onMetricsChanged =
bindingAmbiguate(WidgetsBinding.instance)!.window.onMetricsChanged;
bindingAmbiguate(WidgetsBinding.instance)!.window.onMetricsChanged = () {
if (_onMetricsChanged != null) {
_onMetricsChanged!();
_replaceChild();
setState(() {});
}
};
}
@override
void dispose() {
if (_onMetricsChanged != null) {
bindingAmbiguate(WidgetsBinding.instance)!.window.onMetricsChanged =
_onMetricsChanged;
}
super.dispose();
// Do the cleaning at last.
_umeWidgetState = null;
}
@override
void didUpdateWidget(UMEWidget oldWidget) {
super.didUpdateWidget(oldWidget);
widget.enable
? PluggableMessageService().resetListener()
: PluggableMessageService().clearListener();
if (widget.enable != oldWidget.enable && widget.enable) {
_injectOverlay();
}
if (widget.child != oldWidget.child) {
_replaceChild();
}
if (!widget.enable) {
_removeOverlay();
}
}
void _replaceChild() {
final nestedWidgets =
PluginManager.instance.pluginsMap.values.where((value) {
return value != null && value is PluggableWithNestedWidget;
}).toList();
Widget layoutChild = _buildLayout(
widget.child, widget.supportedLocales, widget.localizationsDelegates);
for (var item in nestedWidgets) {
if (item!.name != PluginManager.instance.activatedPluggableName) {
continue;
}
if (item is PluggableWithNestedWidget) {
layoutChild = item.buildNestedWidget(layoutChild);
break;
}
}
_child =
Directionality(textDirection: TextDirection.ltr, child: layoutChild);
}
Stack _buildLayout(Widget child, Iterable<Locale>? supportedLocales,
Iterable<LocalizationsDelegate> delegates) {
return Stack(
children: <Widget>[
RepaintBoundary(child: child, key: rootKey),
MediaQuery(
data: MediaQueryData.fromWindow(
bindingAmbiguate(WidgetsBinding.instance)!.window),
child: Localizations(
locale: supportedLocales?.first ?? Locale('en', 'US'),
delegates: delegates.toList(),
child: ScaffoldMessenger(child: Overlay(key: overlayKey)),
),
),
],
);
}
void _removeOverlay() {
// Call `remove` only when the entry has been inserted.
if (_overlayEntryInserted) {
_overlayEntry.remove();
_overlayEntryInserted = false;
}
}
void _injectOverlay() {
bindingAmbiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) {
if (_overlayEntryInserted) {
return;
}
if (widget.enable) {
_overlayEntry = OverlayEntry(
builder: (_) => Material(
type: MaterialType.transparency,
child: _ContentPage(
key: _contentPageKey,
refreshChildLayout: () {
_replaceChild();
setState(() {});
},
),
),
);
overlayKey.currentState?.insert(_overlayEntry);
_overlayEntryInserted = true;
}
});
}
@override
Widget build(BuildContext context) => _child;
}
class _ContentPage extends StatefulWidget {
const _ContentPage({Key? key, this.refreshChildLayout}) : super(key: key);
final VoidCallback? refreshChildLayout;
@override
_ContentPageState createState() => _ContentPageState();
}
class _ContentPageState extends State<_ContentPage> {
PluginStoreManager _storeManager = PluginStoreManager();
Size _windowSize = windowSize;
double _dx = 0;
double _dy = 0;
bool _showedMenu = false;
Pluggable? _currentSelected;
Widget _empty = Container();
Widget? _currentWidget;
Widget? _menuPage;
BuildContext? _context;
bool _minimalContent = true;
Widget? _toolbarWidget;
void dragEvent(DragUpdateDetails details) {
_dx = details.globalPosition.dx - dotSize.width / 2;
_dy = details.globalPosition.dy - dotSize.height / 2;
setState(() {});
}
void dragEnd(DragEndDetails details) {
if (_dx + dotSize.width / 2 < _windowSize.width / 2) {
_dx = margin;
} else {
_dx = _windowSize.width - dotSize.width - margin;
}
if (_dy + dotSize.height > _windowSize.height) {
_dy = _windowSize.height - dotSize.height - margin;
} else if (_dy < 0) {
_dy = margin;
}
_storeManager.storeFloatingDotPos(_dx, _dy);
setState(() {});
}
void onTap() {
if (_currentSelected != null) {
_closeActivatedPluggable();
return;
}
_showedMenu = !_showedMenu;
_updatePanelWidget();
}
void _closeActivatedPluggable() {
PluginManager.instance.deactivatePluggable(_currentSelected!);
if (widget.refreshChildLayout != null) {
widget.refreshChildLayout!();
}
_currentSelected = null;
_currentWidget = _empty;
if (_minimalContent) {
_currentWidget = _toolbarWidget;
_showedMenu = true;
}
setState(() {});
}
void _updatePanelWidget() {
setState(() {
_currentWidget =
_showedMenu ? (_minimalContent ? _toolbarWidget : _menuPage) : _empty;
});
}
void _handleAction(BuildContext? context, Pluggable data) {
_currentWidget = data.buildWidget(context);
setState(() {
_showedMenu = false;
});
}
Widget _logoWidget() {
if (_currentSelected != null) {
return Container(
child: Image(image: _currentSelected!.iconImageProvider),
height: 30,
width: 30);
}
return Container(
width: 40, height: 40, color: _showedMenu ? Colors.red : null);
}
@override
void dispose() {
super.dispose();
}
@override
void initState() {
super.initState();
_storeManager.fetchFloatingDotPos().then((value) {
if (value == null || value.split(',').length != 2) {
return;
}
final x = double.parse(value.split(',').first);
final y = double.parse(value.split(',').last);
if (MediaQuery.of(context).size.height - dotSize.height < y ||
MediaQuery.of(context).size.width - dotSize.width < x) {
return;
}
_dx = x;
_dy = y;
setState(() {});
});
_storeManager.fetchMinimalToolbarSwitch().then((value) {
setState(() {
_minimalContent = value ?? true;
});
});
_dx = _windowSize.width - dotSize.width - margin * 4;
_dy = _windowSize.height - dotSize.height - bottomDistance;
MenuAction itemTapAction = (pluginData) async {
if (pluginData is PluggableWithAnywhereDoor) {
dynamic result;
if (pluginData.routeNameAndArgs != null) {
result = await pluginData.navigator?.pushNamed(
pluginData.routeNameAndArgs!.item1,
arguments: pluginData.routeNameAndArgs!.item2);
} else if (pluginData.route != null) {
result = await pluginData.navigator?.push(pluginData.route!);
}
pluginData.popResultReceive(result);
} else {
_currentSelected = pluginData;
if (_currentSelected != null) {
PluginManager.instance.activatePluggable(_currentSelected!);
}
_handleAction(_context, pluginData!);
if (widget.refreshChildLayout != null) {
widget.refreshChildLayout!();
}
pluginData.onTrigger();
}
};
_menuPage = MenuPage(
action: itemTapAction,
minimalAction: () {
_minimalContent = true;
_updatePanelWidget();
PluginStoreManager().storeMinimalToolbarSwitch(true);
},
closeAction: () {
_showedMenu = false;
_updatePanelWidget();
},
);
_toolbarWidget = ToolBarWidget(
action: itemTapAction,
maximalAction: () {
_minimalContent = false;
_updatePanelWidget();
PluginStoreManager().storeMinimalToolbarSwitch(false);
},
closeAction: () {
_showedMenu = false;
_updatePanelWidget();
},
);
_currentWidget = _empty;
}
@override
Widget build(BuildContext context) {
_context = context;
if (_windowSize.isEmpty) {
_dx = MediaQuery.of(context).size.width - dotSize.width - margin * 4;
_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>[
_currentWidget!,
Positioned(
left: _dx,
top: _dy,
child: Tooltip(
message: 'Open ume panel',
child: GestureDetector(
onTap: onTap,
onVerticalDragEnd: dragEnd,
onHorizontalDragEnd: dragEnd,
onHorizontalDragUpdate: dragEvent,
onVerticalDragUpdate: dragEvent,
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
const BoxShadow(
color: Colors.black12,
offset: Offset(0.0, 0.0),
blurRadius: 2.0,
spreadRadius: 1.0)
]),
width: dotSize.width,
height: dotSize.height,
child: Stack(
children: [
Center(
child: _logoWidget(),
),
Positioned(
right: 6,
top: 8,
child: RedDot(
pluginDatas: PluginManager
.instance.pluginsMap.values
.toList(),
))
],
),
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,289 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/ui/icon_cache.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
import 'package:flutter_ume/core/plugin_manager.dart';
import 'package:flutter_ume/core/red_dot.dart';
import 'package:flutter_ume/core/store_manager.dart';
import 'package:flutter_ume/core/pluggable.dart';
import 'package:flutter_ume/util/constants.dart';
class ToolBarWidget extends StatefulWidget {
ToolBarWidget({Key? key, this.action, this.maximalAction, this.closeAction})
: super(key: key);
final MenuAction? action;
final CloseAction? closeAction;
final MaximalAction? maximalAction;
@override
_ToolBarWidgetState createState() => _ToolBarWidgetState();
}
const double _dragBarHeight = 32;
const double _minimalHeight = 80;
class _ToolBarWidgetState extends State<ToolBarWidget> {
double _dy = 0;
late final double _maxDy;
@override
void initState() {
final bottomPadding = WidgetsBinding.instance.window.padding.bottom / ratio;
_maxDy =
windowSize.height - _minimalHeight - _dragBarHeight - bottomPadding;
_dy = _maxDy;
super.initState();
}
void _dragEvent(DragUpdateDetails details) {
_dy += details.delta.dy;
_dy = min(max(0, _dy), _maxDy);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Positioned(
left: 0,
top: _dy,
child: _ToolBarContent(
action: widget.action,
dragCallback: _dragEvent,
maximalAction: widget.maximalAction,
closeAction: widget.closeAction,
),
);
}
}
class _ToolBarContent extends StatefulWidget {
_ToolBarContent(
{Key? key,
this.action,
this.dragCallback,
this.maximalAction,
this.closeAction})
: super(key: key);
final MenuAction? action;
final Function? dragCallback;
final CloseAction? closeAction;
final MaximalAction? maximalAction;
@override
__ToolBarContentState createState() => __ToolBarContentState();
}
class __ToolBarContentState extends State<_ToolBarContent> {
PluginStoreManager _storeManager = PluginStoreManager();
List<Pluggable?> _dataList = [];
@override
void initState() {
super.initState();
_handleData();
}
@override
Widget build(BuildContext context) {
const cornerRadius = Radius.circular(10);
return 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: _minimalHeight + _dragBarHeight,
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!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: const Color(0xff53c22b),
)),
Expanded(
child: GestureDetector(
onVerticalDragUpdate: (details) =>
_dragCallback(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),
),
),
),
),
),
],
),
),
),
_PluginScrollContainer(
dataList: _dataList,
action: widget.action,
),
],
),
),
);
}
_dragCallback(DragUpdateDetails details) {
if (widget.dragCallback != null) widget.dragCallback!(details);
}
void _handleData() async {
List<Pluggable?> dataList = [];
List<String>? list = await _storeManager.fetchStorePlugins();
if (list == null || list.isEmpty) {
dataList = PluginManager.instance.pluginsMap.values.toList();
} else {
list.forEach((f) {
bool contain = PluginManager.instance.pluginsMap.containsKey(f);
if (contain) {
dataList.add(PluginManager.instance.pluginsMap[f]);
}
});
PluginManager.instance.pluginsMap.keys.forEach((key) {
if (!list.contains(key)) {
dataList.add(PluginManager.instance.pluginsMap[key]);
}
});
}
_saveData(dataList);
setState(() {
_dataList = dataList;
});
}
void _saveData(List<Pluggable?> data) {
List l = data.map((f) => f!.name).toList();
if (l.isEmpty) {
return;
}
Future.delayed(Duration(milliseconds: 500), () {
_storeManager.storePlugins(l as List<String>);
});
}
}
class _PluginScrollContainer extends StatelessWidget {
_PluginScrollContainer({Key? key, required this.dataList, this.action})
: super(key: key);
final List<Pluggable?> dataList;
final MenuAction? action;
@override
Widget build(BuildContext context) {
return Container(
height: _minimalHeight,
width: MediaQuery.of(context).size.width,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: dataList
.map(
(data) => _MenuCell(
pluginData: data,
action: action,
),
)
.toList(),
)));
}
}
class _MenuCell extends StatelessWidget {
const _MenuCell({Key? key, this.pluginData, this.action}) : super(key: key);
final Pluggable? pluginData;
final MenuAction? action;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
PluggableMessageService().resetCounter(pluginData!);
if (action != null) {
action!(pluginData);
}
},
child: Stack(
children: [
Container(
height: _minimalHeight,
width: _minimalHeight,
color: Colors.white,
child: Container(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: IconCache.icon(pluggableInfo: pluginData!),
height: 28,
width: 28),
Container(
margin: const EdgeInsets.only(top: 4),
child: Text(
pluginData!.name,
style:
const TextStyle(fontSize: 12, color: Colors.black),
maxLines: 1,
))
],
),
),
),
Positioned(
right: 8,
top: 8,
child: RedDot(
pluginDatas: [pluginData],
),
),
],
),
);
}
}
@@ -0,0 +1,9 @@
library flutter_ume;
export 'core/ui/root_widget.dart';
export 'core/plugin_manager.dart';
export 'core/pluggable.dart';
export 'core/ui/global.dart';
export 'service/inspector/inspector_overlay.dart';
export 'service/vm_service/service_mixin.dart';
export 'service/vm_service/service_wrapper.dart';
@@ -0,0 +1,334 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter_ume/util/constants.dart';
class InspectorOverlay extends LeafRenderObjectWidget {
const InspectorOverlay(
{Key? key,
required this.selection,
this.needEdges = true,
this.needDescription = true})
: super(key: key);
final InspectorSelection selection;
final bool needDescription;
final bool needEdges;
@override
_RenderInspectorOverlay createRenderObject(BuildContext context) {
return _RenderInspectorOverlay(
selection: selection,
needDescription: needDescription,
needEdges: needEdges);
}
@override
void updateRenderObject(
BuildContext context, _RenderInspectorOverlay renderObject) {
renderObject.selection = selection;
}
}
class _RenderInspectorOverlay extends RenderBox {
_RenderInspectorOverlay({
required InspectorSelection selection,
required this.needDescription,
required this.needEdges,
}) : _selection = selection;
final bool needDescription;
final bool needEdges;
InspectorSelection get selection => _selection;
InspectorSelection _selection;
set selection(InspectorSelection value) {
if (value != _selection) {
_selection = value;
}
markNeedsPaint();
}
@override
bool get sizedByParent => true;
@override
bool get alwaysNeedsCompositing => true;
@override
void performResize() {
size = constraints.constrain(const Size(double.infinity, double.infinity));
}
@override
void paint(PaintingContext context, Offset offset) {
assert(needsCompositing);
context.addLayer(_InspectorOverlayLayer(
needEdges: needEdges,
needDescription: needDescription,
overlayRect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
selection: selection,
));
}
}
class _InspectorOverlayLayer extends Layer {
_InspectorOverlayLayer({
required this.overlayRect,
required this.selection,
required this.needDescription,
required this.needEdges,
});
InspectorSelection selection;
final bool needDescription;
final bool needEdges;
final Rect overlayRect;
_InspectorOverlayRenderState? _lastState;
late ui.Picture _picture;
TextPainter? _textPainter;
double? _textPainterMaxWidth;
@override
void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
if (!selection.active) return;
final _SelectionInfo info = _SelectionInfo(selection);
final RenderObject? selected = info.renderObject;
final List<_TransformedRect> candidates = <_TransformedRect>[];
for (RenderObject candidate in selection.candidates) {
if (candidate == selected || !candidate.attached) continue;
candidates.add(_TransformedRect(candidate));
}
final _InspectorOverlayRenderState state = _InspectorOverlayRenderState(
selectionInfo: info,
overlayRect: overlayRect,
selected: _TransformedRect(selected!),
textDirection: TextDirection.ltr,
candidates: candidates,
);
if (state != _lastState) {
_lastState = state;
_picture = _buildPicture(state);
}
builder.addPicture(layerOffset, _picture);
}
ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas canvas = Canvas(recorder, state.overlayRect);
final Size size = state.overlayRect.size;
final Paint fillPaint = Paint()
..style = PaintingStyle.fill
..color = kHighlightedRenderObjectFillColor;
final Paint borderPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = kHighlightedRenderObjectBorderColor;
final Rect selectedPaintRect = state.selected.rect.deflate(0.5);
canvas
..save()
..transform(state.selected.transform.storage)
..drawRect(selectedPaintRect, fillPaint)
..drawRect(selectedPaintRect, borderPaint)
..restore();
if (needEdges) {
for (_TransformedRect transformedRect in state.candidates) {
canvas
..save()
..transform(transformedRect.transform.storage)
..drawRect(transformedRect.rect.deflate(0.5), borderPaint)
..restore();
}
}
final Rect targetRect = MatrixUtils.transformRect(
state.selected.transform, state.selected.rect);
final Offset target = Offset(targetRect.left, targetRect.center.dy);
const double offsetFromWidget = 9.0;
final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
if (needDescription) {
_paintDescription(canvas, state.selectionInfo.message,
state.textDirection, target, verticalOffset, size, targetRect);
}
return recorder.endRecording();
}
void _paintDescription(
Canvas canvas,
String message,
TextDirection textDirection,
Offset target,
double verticalOffset,
Size size,
Rect targetRect,
) {
canvas.save();
final double maxWidth =
size.width - 2 * (kScreenEdgeMargin + kTooltipPadding);
final TextSpan? textSpan = _textPainter?.text as TextSpan?;
if (_textPainter == null ||
textSpan!.text != message ||
_textPainterMaxWidth != maxWidth) {
_textPainterMaxWidth = maxWidth;
_textPainter = TextPainter()
..maxLines = kMaxTooltipLines
..ellipsis = '...'
..text = TextSpan(
style: TextStyle(color: kTipTextColor, fontSize: 12.0, height: 1.2),
text: message)
..textDirection = textDirection
..layout(maxWidth: maxWidth);
}
final Size tooltipSize = _textPainter!.size +
const Offset(kTooltipPadding * 2, kTooltipPadding * 2);
final Offset tipOffset = positionDependentBox(
size: size,
childSize: tooltipSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: false,
);
final Paint tooltipBackground = Paint()
..style = PaintingStyle.fill
..color = kTooltipBackgroundColor;
canvas.drawRect(
Rect.fromPoints(
tipOffset,
tipOffset.translate(tooltipSize.width, tooltipSize.height),
),
tooltipBackground,
);
double wedgeY = tipOffset.dy;
final bool tooltipBelow = tipOffset.dy > target.dy;
if (!tooltipBelow) wedgeY += tooltipSize.height;
const double wedgeSize = kTooltipPadding * 2;
double wedgeX = math.max(tipOffset.dx, target.dx) + wedgeSize * 2;
wedgeX = math.min(wedgeX, tipOffset.dx + tooltipSize.width - wedgeSize * 2);
final List<Offset> wedge = <Offset>[
Offset(wedgeX - wedgeSize, wedgeY),
Offset(wedgeX + wedgeSize, wedgeY),
Offset(wedgeX, wedgeY + (tooltipBelow ? -wedgeSize : wedgeSize)),
];
canvas.drawPath(Path()..addPolygon(wedge, true), tooltipBackground);
_textPainter!.paint(
canvas, tipOffset + const Offset(kTooltipPadding, kTooltipPadding));
canvas.restore();
}
@override
@protected
bool findAnnotations<S extends Object>(
AnnotationResult<S> result, Offset localPosition,
{required bool onlyFirst}) {
return false;
}
}
class _SelectionInfo {
const _SelectionInfo(this.selection);
final InspectorSelection selection;
RenderObject? get renderObject => selection.current;
Element? get element => selection.currentElement;
Map? get jsonInfo {
if (renderObject == null) return null;
final widgetId = WidgetInspectorService.instance
// ignore: invalid_use_of_protected_member
.toId(renderObject!.toDiagnosticsNode(), '');
if (widgetId == null) return null;
String infoStr =
WidgetInspectorService.instance.getSelectedSummaryWidget(widgetId, '');
return json.decode(infoStr);
}
String? get filePath {
final f = (jsonInfo != null && jsonInfo!.containsKey('creationLocation'))
? jsonInfo!['creationLocation']['file']
: '';
return f;
}
int? get line {
final l = (jsonInfo != null && jsonInfo!.containsKey('creationLocation'))
? jsonInfo!['creationLocation']['line']
: 0;
return l;
}
String get message {
return '''${element!.toStringShort()}\nsize: ${renderObject!.paintBounds.size}\nfilePath: $filePath\nline: $line''';
}
}
class _InspectorOverlayRenderState {
_InspectorOverlayRenderState({
required this.overlayRect,
required this.selected,
required this.candidates,
required this.textDirection,
required this.selectionInfo,
});
final Rect overlayRect;
final _TransformedRect selected;
final List<_TransformedRect> candidates;
final TextDirection textDirection;
final _SelectionInfo selectionInfo;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final _InspectorOverlayRenderState typedOther = other;
return overlayRect == typedOther.overlayRect &&
selected == typedOther.selected &&
listEquals<_TransformedRect>(candidates, typedOther.candidates);
}
@override
int get hashCode =>
Object.hash(overlayRect, selected, Object.hashAll(candidates));
}
class _TransformedRect {
_TransformedRect(RenderObject object)
: rect = object.semanticBounds,
transform = object.getTransformTo(null);
final Rect rect;
final Matrix4 transform;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final _TransformedRect typedOther = other;
return rect == typedOther.rect && transform == typedOther.transform;
}
@override
int get hashCode => Object.hash(rect, transform);
}
@@ -0,0 +1,5 @@
import 'package:flutter_ume/service/vm_service/service_wrapper.dart';
mixin VMServiceWrapper {
final ServiceWrapper serviceWrapper = ServiceWrapper();
}
@@ -0,0 +1,107 @@
import 'dart:developer';
import 'dart:isolate';
import 'package:vm_service/utils.dart';
import 'package:vm_service/vm_service.dart' as vm;
import 'package:vm_service/vm_service_io.dart';
class ServiceWrapper {
vm.VmService? _service;
String? _isolateId;
String? get isolateId {
if (_isolateId != null) {
return _isolateId;
}
_isolateId = Service.getIsolateID(Isolate.current);
return _isolateId;
}
Future<vm.VmService> getVMService() async {
if (_service != null) {
return _service!;
}
ServiceProtocolInfo info = await Service.getInfo();
String url = info.serverUri.toString();
Uri uri = Uri.parse(url);
Uri socketUri = convertToWebSocketUrl(serviceProtocolUrl: uri);
_service = await vmServiceConnectUri(socketUri.toString());
return _service!;
}
Future<vm.VM> getVM() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getVM();
}
Future<vm.MemoryUsage> getMemoryUsage() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getMemoryUsage(isolateId!);
}
Future<vm.ClassList> getClassList() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getClassList(isolateId!);
}
Future<vm.AllocationProfile> getAllocationProfile() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getAllocationProfile(isolateId!, reset: true);
}
Future<vm.Isolate> getIsolate() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getIsolate(isolateId!);
}
Future<List<vm.LibraryRef>?> getLibraries() async {
vm.Isolate isolate = await getIsolate();
return isolate.libraries;
}
Future<vm.HeapSnapshotGraph> getSnapshot() async {
vm.VmService virtualMachine = await getVMService();
vm.Isolate isolate = await getIsolate();
return vm.HeapSnapshotGraph.getSnapshot(virtualMachine, isolate);
}
Future<vm.InstanceSet> getInstances(String objectId, int limit) async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getInstances(isolateId!, objectId, limit);
}
Future<vm.Stack> getStack() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getStack(isolateId!);
}
Future<vm.Obj> getObject(String objectId, {int? offset, int? count}) async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getObject(isolateId!, objectId,
offset: offset, count: count);
}
Future<vm.InboundReferences> getInboundReferences(String objectId) async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getInboundReferences(isolateId!, objectId, 100);
}
Future<List<vm.ClassHeapStats>> getClassHeapStats() async {
vm.AllocationProfile profile = await getAllocationProfile();
List<vm.ClassHeapStats> list = profile.members!
.where((element) =>
element.bytesCurrent! > 0 || element.instancesCurrent! > 0)
.toList();
return list;
}
Future<vm.ScriptList> getScripts() async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.getScripts(isolateId!);
}
Future<vm.Response> evaluate(String targetId, String expression) async {
vm.VmService virtualMachine = await getVMService();
return virtualMachine.evaluate(isolateId!, targetId, expression);
}
}
@@ -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);
}
}