新增网络可视化请求工具
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user