新增网络可视化请求工具

This commit is contained in:
2026-07-22 09:24:48 +08:00
parent 4ff93edaab
commit d6e80df5bf
230 changed files with 92710 additions and 51 deletions
@@ -0,0 +1,30 @@
import 'package:flutter/widgets.dart';
import 'package:tuple/tuple.dart';
abstract class Pluggable {
String get name;
String get displayName;
void onTrigger();
Widget? buildWidget(BuildContext? context);
ImageProvider get iconImageProvider;
}
typedef StreamFilter = bool Function(dynamic);
abstract class PluggableWithStream extends Pluggable {
Stream get stream;
StreamFilter get streamFilter;
}
abstract class PluggableWithNestedWidget extends Pluggable {
Widget buildNestedWidget(Widget child);
}
abstract class PluggableWithAnywhereDoor extends Pluggable {
NavigatorState? get navigator;
Tuple2<String, Object?>? get routeNameAndArgs;
Route? get route;
void popResultReceive(dynamic result);
}
@@ -0,0 +1,99 @@
import 'dart:async';
import 'package:flutter_ume/core/plugin_manager.dart';
import 'package:flutter_ume/core/pluggable.dart';
class PluggableMessageService {
static final PluggableMessageService _instance =
PluggableMessageService._internal();
factory PluggableMessageService() {
return _instance;
}
// ignore: close_sinks
StreamController<PluggableMessage> messageStreamController =
StreamController.broadcast();
Map<String, PluggableMessageInfo> get pluggableMessageData =>
_pluggableMessageData;
Map<String, PluggableMessageInfo> _pluggableMessageData = Map();
PluggableMessageService._internal() {
_pluggableMessageData = Map();
}
void resetListener() {
clearListener();
PluginManager.instance.pluginsMap.values
.whereType<PluggableWithStream>()
.forEach((element) {
final pluggable = element;
// ignore: cancel_subscriptions
final subscription = pluggable.stream.where((event) {
return pluggable.streamFilter(event);
}).listen((event) {
_pluggableMessageData[pluggable.name]!.increaseCounter();
_sendSink(pluggable);
});
_pluggableMessageData.update(pluggable.name, (old) {
old.subscription?.cancel();
return PluggableMessageInfo.subscription(subscription);
}, ifAbsent: () => PluggableMessageInfo.subscription(subscription));
});
}
int countAll(List<Pluggable?> pluggable) {
int result = 0;
pluggable.map((e) => e!.name).toSet().forEach((element) {
result += (_pluggableMessageData[element]?.count ?? 0);
});
return result;
}
int count(Pluggable pluggable) =>
_pluggableMessageData[pluggable.name]?.count ?? 0;
void resetCounter(Pluggable pluggable) {
_pluggableMessageData[pluggable.name]?.resetCounter();
_sendSink(pluggable);
}
void clearListener() {
_pluggableMessageData.values.forEach((messageInfo) {
messageInfo.subscription?.cancel();
});
_pluggableMessageData.clear();
}
void _sendSink(Pluggable pluggable) {
messageStreamController.sink.add(PluggableMessage.create(
pluggable.name, PluggableMessageService().count(pluggable)));
}
}
class PluggableMessageInfo {
StreamSubscription<dynamic>? _subscription;
int _count = 0;
StreamSubscription<dynamic>? get subscription => _subscription;
int get count => _count;
PluggableMessageInfo.subscription(StreamSubscription<dynamic> subscription) {
_subscription = subscription;
_count = 0;
}
void increaseCounter() {
_count++;
}
void resetCounter() {
_count = 0;
}
}
class PluggableMessage {
int _count;
String _key;
int get count => _count;
String get key => _key;
PluggableMessage.create(this._key, this._count);
}
@@ -0,0 +1,47 @@
import 'package:flutter_ume/flutter_ume.dart';
import 'package:flutter_ume/core/pluggable.dart';
class PluginManager {
static PluginManager? _instance;
Map<String, Pluggable?> get pluginsMap => _pluginsMap;
Map<String, Pluggable?> _pluginsMap = {};
Pluggable? _activatedPluggable;
String? get activatedPluggableName => _activatedPluggable?.name;
static PluginManager get instance {
if (_instance == null) {
_instance = PluginManager._();
}
return _instance!;
}
PluginManager._();
/// Register a single [plugin]
void register(Pluggable plugin) {
if (plugin.name.isEmpty) {
return;
}
_pluginsMap[plugin.name] = plugin;
}
/// Register multiple [plugins]
void registerAll(List<Pluggable> plugins) {
for (final plugin in plugins) {
register(plugin);
}
}
void activatePluggable(Pluggable pluggable) {
_activatedPluggable = pluggable;
}
void deactivatePluggable(Pluggable pluggable) {
if (_activatedPluggable?.name == pluggable.name) {
_activatedPluggable = null;
}
}
}
@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/pluggable.dart';
class RedDot extends StatefulWidget {
RedDot({Key? key, required this.pluginDatas, this.size = 16})
: super(key: key);
final List<Pluggable?> pluginDatas;
final double size;
@override
_RedDotState createState() => _RedDotState();
}
class _RedDotState extends State<RedDot> {
int _count = 0;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_subscription =
PluggableMessageService().messageStreamController.stream.listen((data) {
if (mounted &&
widget.pluginDatas.any((element) => element!.name == data.key)) {
_refresh();
}
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_refresh();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
_refresh() {
setState(() {
_count = PluggableMessageService().countAll(widget.pluginDatas);
});
}
@override
Widget build(BuildContext context) {
if (_count == 0) {
return Container();
}
return Container(
height: widget.size,
child: Padding(
padding: EdgeInsets.only(
left: widget.size * 0.28, right: widget.size * 0.28),
child: Directionality(
textDirection: TextDirection.ltr,
child: Text(
_count.toString(),
style: TextStyle(color: Colors.white, fontSize: widget.size * 0.8),
textAlign: TextAlign.center,
),
),
),
decoration: ShapeDecoration(
color: Colors.red,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(widget.size * 0.5)))),
);
}
}
@@ -0,0 +1,42 @@
import 'package:shared_preferences/shared_preferences.dart';
class PluginStoreManager {
final String _pluginStoreKey = 'PluginStoreKey';
final String _minimalToolbarSwitch = 'MinimalToolbarSwitch';
final String _floatingDotPos = 'FloatingDotPos';
Future<SharedPreferences> _sharedPref = SharedPreferences.getInstance();
Future<List<String>?> fetchStorePlugins() async {
final SharedPreferences prefs = await _sharedPref;
return prefs.getStringList(_pluginStoreKey);
}
void storePlugins(List<String> plugins) async {
if (plugins.isEmpty) {
return;
}
final SharedPreferences prefs = await _sharedPref;
await prefs.setStringList(_pluginStoreKey, plugins);
}
Future<bool?> fetchMinimalToolbarSwitch() async {
final SharedPreferences prefs = await _sharedPref;
return prefs.getBool(_minimalToolbarSwitch);
}
void storeMinimalToolbarSwitch(bool value) async {
final SharedPreferences prefs = await _sharedPref;
await prefs.setBool(_minimalToolbarSwitch, value);
}
Future<String?> fetchFloatingDotPos() async {
final SharedPreferences prefs = await _sharedPref;
return prefs.getString(_floatingDotPos);
}
void storeFloatingDotPos(double x, double y) async {
final SharedPreferences prefs = await _sharedPref;
prefs.setString(_floatingDotPos, "$x,$y");
}
}
@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
typedef CanAccept = bool Function(int oldIndex, int newIndex);
typedef DragCompletion<T> = void Function(List<T?>? data);
typedef DataWidgetBuilder<T> = Widget Function(BuildContext context, T data);
class DragableGridView<T> extends StatefulWidget {
final DataWidgetBuilder<T> itemBuilder;
final CanAccept canAccept;
final List<T> dataList;
final int crossAxisCount;
final Axis scrollDirection;
final double childAspectRatio;
final DragCompletion? dragCompletion;
DragableGridView(
this.dataList, {
Key? key,
this.scrollDirection = Axis.vertical,
this.crossAxisCount = 3,
this.childAspectRatio = 1.0,
this.dragCompletion,
required this.itemBuilder,
required this.canAccept,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _DragableGridViewState<T>();
}
class _DragableGridViewState<T> extends State<DragableGridView> {
List<T?>? dataList;
late List<T?> dataListBackup;
bool showItemWhenCovered = false;
int willAcceptIndex = -1;
int draggingItemIndex = -1;
@override
void initState() {
super.initState();
dataList = widget.dataList as List<T?>?;
dataListBackup = dataList!.sublist(0);
}
@override
Widget build(BuildContext context) {
return GridView.builder(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: dataList!.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, childAspectRatio: 0.85),
itemBuilder: ((ctx, index) {
return _buildDraggable(ctx, index);
}));
}
Widget _buildDraggable(BuildContext context, int index) {
return LayoutBuilder(
builder: (context, constraint) {
return LongPressDraggable(
data: index,
child: DragTarget<int>(
onAccept: (_) {},
builder: (context, data, rejects) {
return willAcceptIndex >= 0 && willAcceptIndex == index
? Container()
: widget.itemBuilder(context, dataList![index]);
},
onLeave: (_) {
willAcceptIndex = -1;
setState(() {
showItemWhenCovered = false;
dataList = dataListBackup.sublist(0);
});
},
onWillAccept: (int? fromIndex) {
final accept = fromIndex != index;
if (accept) {
willAcceptIndex = index;
showItemWhenCovered = true;
dataList = dataListBackup.sublist(0);
final fromData = dataList![fromIndex!];
setState(() {
dataList!.removeAt(fromIndex);
dataList!.insert(index, fromData);
});
}
return accept;
},
),
onDragStarted: () {
draggingItemIndex = index;
dataListBackup = dataList!.sublist(0);
},
onDraggableCanceled: (Velocity velocity, Offset offset) {
setState(() {
willAcceptIndex = -1;
showItemWhenCovered = false;
dataList = dataListBackup.sublist(0);
});
},
onDragCompleted: () {
if (widget.dragCompletion != null) {
widget.dragCompletion!(dataList);
}
setState(() {
showItemWhenCovered = false;
willAcceptIndex = -1;
});
},
feedback: Container(
child: SizedBox(
width: constraint.maxWidth,
height: constraint.maxHeight,
child: widget.itemBuilder(context, dataList![index]),
),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 18,
spreadRadius: 0.8,
color: Colors.black87,
),
],
borderRadius: BorderRadius.circular(6),
),
),
childWhenDragging: Container(
child: SizedBox(
child: showItemWhenCovered
? widget.itemBuilder(context, dataList![index])
: null,
),
),
);
},
);
}
}
@@ -0,0 +1,6 @@
import 'package:flutter/widgets.dart';
final GlobalKey rootKey = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'ume_navigator');
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/pluggable.dart';
class IconCache {
static Map<String, Widget> _icons = Map();
static Widget? icon({
required Pluggable pluggableInfo,
}) {
if (!_icons.containsKey(pluggableInfo.name)) {
final i = Image(image: pluggableInfo.iconImageProvider);
_icons.putIfAbsent(pluggableInfo.name, () => i);
} else if (!_icons.containsKey(pluggableInfo.name)) {
return Container();
}
return _icons[pluggableInfo.name];
}
}
@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/ui/icon_cache.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/red_dot.dart';
import 'package:flutter_ume/core/store_manager.dart';
import 'package:flutter_ume/flutter_ume.dart';
import 'dragable_widget.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
class MenuPage extends StatefulWidget {
MenuPage({Key? key, this.action, this.minimalAction, this.closeAction})
: super(key: key);
final MenuAction? action;
final MinimalAction? minimalAction;
final CloseAction? closeAction;
@override
_MenuPageState createState() => _MenuPageState();
}
class _MenuPageState extends State<MenuPage>
with SingleTickerProviderStateMixin {
PluginStoreManager _storeManager = PluginStoreManager();
List<Pluggable?> _dataList = [];
@override
void initState() {
super.initState();
_handleData();
}
void _handleData() async {
List<Pluggable?> dataList = [];
List<String>? list = await _storeManager.fetchStorePlugins();
if (list == null || list.isEmpty) {
dataList = PluginManager.instance.pluginsMap.values.toList();
} else {
list.forEach((f) {
bool contain = PluginManager.instance.pluginsMap.containsKey(f);
if (contain) {
dataList.add(PluginManager.instance.pluginsMap[f]);
}
});
PluginManager.instance.pluginsMap.keys.forEach((key) {
if (!list.contains(key)) {
dataList.add(PluginManager.instance.pluginsMap[key]);
}
});
}
_saveData(dataList);
setState(() {
_dataList = dataList;
});
}
void _saveData(List<Pluggable?> data) {
List l = data.map((f) => f!.name).toList();
if (l.isEmpty) {
return;
}
Future.delayed(Duration(milliseconds: 500), () {
_storeManager.storePlugins(l as List<String>);
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
bottom: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Colors.white,
height: 100,
width: MediaQuery.of(context).size.width,
alignment: Alignment.bottomLeft,
padding: const EdgeInsets.only(left: 16, right: 16),
margin: const EdgeInsets.only(bottom: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
InkWell(
onTap: () {
if (widget.closeAction != null) {
widget.closeAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: Color(0xffff5a52),
)),
const SizedBox(
width: 8,
),
InkWell(
onTap: () {
if (widget.minimalAction != null) {
widget.minimalAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: Color(0xffe6c029),
)),
],
),
Container(
child: Text('UME',
style: const TextStyle(
fontSize: 60,
fontWeight: FontWeight.w800,
color: Color(0xff454545)))),
],
),
),
Expanded(
child: _dataList.isEmpty
? _EmptyPlaceholder()
: DragableGridView(
_dataList,
childAspectRatio: 0.85,
canAccept: (oldIndex, newIndex) {
return true;
},
dragCompletion: (dataList) {
_saveData(dataList as List<Pluggable?>);
},
itemBuilder: (context, dynamic data) {
return GestureDetector(
onTap: () {
widget.action!(data);
PluggableMessageService().resetCounter(data);
},
behavior: HitTestBehavior.opaque,
child: _MenuCell(pluginData: data),
);
},
))
],
),
),
);
}
}
class _EmptyPlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text('Empty'),
);
}
}
class _MenuCell extends StatelessWidget {
const _MenuCell({Key? key, this.pluginData}) : super(key: key);
final Pluggable? pluginData;
@override
Widget build(BuildContext context) {
final Color lineColor = Colors.grey.withOpacity(0.25);
return LayoutBuilder(builder: (_, constraints) {
return Material(
color: Colors.white,
child: Container(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
left: 0,
top: 0,
child: Container(
height: constraints.maxHeight,
width: 0.5,
color: lineColor)),
Positioned(
left: 0,
top: 0,
child: Container(
height: 0.5,
width: constraints.maxWidth,
color: lineColor)),
Positioned(
top: 0,
right: 0,
child: Container(
height: constraints.maxHeight,
width: 0.5,
color: lineColor)),
Positioned(
bottom: 0,
left: 0,
child: Container(
height: 0.5,
width: constraints.maxWidth,
color: lineColor)),
Container(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: IconCache.icon(pluggableInfo: pluginData!),
height: 40,
width: 40),
Container(
margin: const EdgeInsets.only(top: 25),
child: Text(pluginData!.displayName,
style: const TextStyle(
fontSize: 15, color: Colors.black)))
],
),
),
Positioned(
right: 12,
top: 12,
child: RedDot(
pluginDatas: [pluginData],
size: 22,
),
),
],
),
),
);
});
}
}
@@ -0,0 +1,6 @@
import 'package:flutter_ume/core/pluggable.dart';
typedef MenuAction = void Function(Pluggable?);
typedef MinimalAction = void Function();
typedef MaximalAction = void Function();
typedef CloseAction = void Function();
@@ -0,0 +1,437 @@
import 'package:flutter/material.dart'
hide FlutterLogo, FlutterLogoDecoration, FlutterLogoStyle;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_ume/core/pluggable.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/plugin_manager.dart';
import 'package:flutter_ume/core/red_dot.dart';
import 'package:flutter_ume/core/store_manager.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
import 'package:flutter_ume/core/ui/toolbar_widget.dart';
import 'package:flutter_ume/util/binding_ambiguate.dart';
import 'package:flutter_ume/util/constants.dart';
import './menu_page.dart';
import 'global.dart';
const defaultLocalizationsDelegates = const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
];
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
/// Wrap your App widget. If [enable] is false, the function will return [child].
class UMEWidget extends StatefulWidget {
const UMEWidget({
Key? key,
required this.child,
this.enable = true,
this.supportedLocales,
this.localizationsDelegates = defaultLocalizationsDelegates,
}) : super(key: key);
final Widget child;
final bool enable;
final Iterable<Locale>? supportedLocales;
final Iterable<LocalizationsDelegate> localizationsDelegates;
/// Close the activated plugin if any.
///
/// The method does not have side-effects whether the [UMEWidget]
/// is not enabled or no plugin has been activated.
static void closeActivatedPlugin() {
final _ContentPageState? state =
_umeWidgetState?._contentPageKey.currentState;
if (state?._currentSelected != null) {
state?._closeActivatedPluggable();
}
}
@override
_UMEWidgetState createState() => _UMEWidgetState();
}
/// Hold the [_UMEWidgetState] as a global variable.
_UMEWidgetState? _umeWidgetState;
class _UMEWidgetState extends State<UMEWidget> {
_UMEWidgetState() {
// Make sure only a single `UMEWidget` is being used.
assert(
_umeWidgetState == null,
'Only one `UMEWidget` can be used at the same time.',
);
if (_umeWidgetState != null) {
throw StateError('Only one `UMEWidget` can be used at the same time.');
}
_umeWidgetState = this;
}
final GlobalKey<_ContentPageState> _contentPageKey = GlobalKey();
late Widget _child;
VoidCallback? _onMetricsChanged;
bool _overlayEntryInserted = false;
OverlayEntry _overlayEntry = OverlayEntry(
builder: (_) => const SizedBox.shrink(),
);
@override
void initState() {
super.initState();
_replaceChild();
_injectOverlay();
_onMetricsChanged =
bindingAmbiguate(WidgetsBinding.instance)!.window.onMetricsChanged;
bindingAmbiguate(WidgetsBinding.instance)!.window.onMetricsChanged = () {
if (_onMetricsChanged != null) {
_onMetricsChanged!();
_replaceChild();
setState(() {});
}
};
}
@override
void dispose() {
if (_onMetricsChanged != null) {
bindingAmbiguate(WidgetsBinding.instance)!.window.onMetricsChanged =
_onMetricsChanged;
}
super.dispose();
// Do the cleaning at last.
_umeWidgetState = null;
}
@override
void didUpdateWidget(UMEWidget oldWidget) {
super.didUpdateWidget(oldWidget);
widget.enable
? PluggableMessageService().resetListener()
: PluggableMessageService().clearListener();
if (widget.enable != oldWidget.enable && widget.enable) {
_injectOverlay();
}
if (widget.child != oldWidget.child) {
_replaceChild();
}
if (!widget.enable) {
_removeOverlay();
}
}
void _replaceChild() {
final nestedWidgets =
PluginManager.instance.pluginsMap.values.where((value) {
return value != null && value is PluggableWithNestedWidget;
}).toList();
Widget layoutChild = _buildLayout(
widget.child, widget.supportedLocales, widget.localizationsDelegates);
for (var item in nestedWidgets) {
if (item!.name != PluginManager.instance.activatedPluggableName) {
continue;
}
if (item is PluggableWithNestedWidget) {
layoutChild = item.buildNestedWidget(layoutChild);
break;
}
}
_child =
Directionality(textDirection: TextDirection.ltr, child: layoutChild);
}
Stack _buildLayout(Widget child, Iterable<Locale>? supportedLocales,
Iterable<LocalizationsDelegate> delegates) {
return Stack(
children: <Widget>[
RepaintBoundary(child: child, key: rootKey),
MediaQuery(
data: MediaQueryData.fromWindow(
bindingAmbiguate(WidgetsBinding.instance)!.window),
child: Localizations(
locale: supportedLocales?.first ?? Locale('en', 'US'),
delegates: delegates.toList(),
child: ScaffoldMessenger(child: Overlay(key: overlayKey)),
),
),
],
);
}
void _removeOverlay() {
// Call `remove` only when the entry has been inserted.
if (_overlayEntryInserted) {
_overlayEntry.remove();
_overlayEntryInserted = false;
}
}
void _injectOverlay() {
bindingAmbiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) {
if (_overlayEntryInserted) {
return;
}
if (widget.enable) {
_overlayEntry = OverlayEntry(
builder: (_) => Material(
type: MaterialType.transparency,
child: _ContentPage(
key: _contentPageKey,
refreshChildLayout: () {
_replaceChild();
setState(() {});
},
),
),
);
overlayKey.currentState?.insert(_overlayEntry);
_overlayEntryInserted = true;
}
});
}
@override
Widget build(BuildContext context) => _child;
}
class _ContentPage extends StatefulWidget {
const _ContentPage({Key? key, this.refreshChildLayout}) : super(key: key);
final VoidCallback? refreshChildLayout;
@override
_ContentPageState createState() => _ContentPageState();
}
class _ContentPageState extends State<_ContentPage> {
PluginStoreManager _storeManager = PluginStoreManager();
Size _windowSize = windowSize;
double _dx = 0;
double _dy = 0;
bool _showedMenu = false;
Pluggable? _currentSelected;
Widget _empty = Container();
Widget? _currentWidget;
Widget? _menuPage;
BuildContext? _context;
bool _minimalContent = true;
Widget? _toolbarWidget;
void dragEvent(DragUpdateDetails details) {
_dx = details.globalPosition.dx - dotSize.width / 2;
_dy = details.globalPosition.dy - dotSize.height / 2;
setState(() {});
}
void dragEnd(DragEndDetails details) {
if (_dx + dotSize.width / 2 < _windowSize.width / 2) {
_dx = margin;
} else {
_dx = _windowSize.width - dotSize.width - margin;
}
if (_dy + dotSize.height > _windowSize.height) {
_dy = _windowSize.height - dotSize.height - margin;
} else if (_dy < 0) {
_dy = margin;
}
_storeManager.storeFloatingDotPos(_dx, _dy);
setState(() {});
}
void onTap() {
if (_currentSelected != null) {
_closeActivatedPluggable();
return;
}
_showedMenu = !_showedMenu;
_updatePanelWidget();
}
void _closeActivatedPluggable() {
PluginManager.instance.deactivatePluggable(_currentSelected!);
if (widget.refreshChildLayout != null) {
widget.refreshChildLayout!();
}
_currentSelected = null;
_currentWidget = _empty;
if (_minimalContent) {
_currentWidget = _toolbarWidget;
_showedMenu = true;
}
setState(() {});
}
void _updatePanelWidget() {
setState(() {
_currentWidget =
_showedMenu ? (_minimalContent ? _toolbarWidget : _menuPage) : _empty;
});
}
void _handleAction(BuildContext? context, Pluggable data) {
_currentWidget = data.buildWidget(context);
setState(() {
_showedMenu = false;
});
}
Widget _logoWidget() {
if (_currentSelected != null) {
return Container(
child: Image(image: _currentSelected!.iconImageProvider),
height: 30,
width: 30);
}
return Container(
width: 40, height: 40, color: _showedMenu ? Colors.red : null);
}
@override
void dispose() {
super.dispose();
}
@override
void initState() {
super.initState();
_storeManager.fetchFloatingDotPos().then((value) {
if (value == null || value.split(',').length != 2) {
return;
}
final x = double.parse(value.split(',').first);
final y = double.parse(value.split(',').last);
if (MediaQuery.of(context).size.height - dotSize.height < y ||
MediaQuery.of(context).size.width - dotSize.width < x) {
return;
}
_dx = x;
_dy = y;
setState(() {});
});
_storeManager.fetchMinimalToolbarSwitch().then((value) {
setState(() {
_minimalContent = value ?? true;
});
});
_dx = _windowSize.width - dotSize.width - margin * 4;
_dy = _windowSize.height - dotSize.height - bottomDistance;
MenuAction itemTapAction = (pluginData) async {
if (pluginData is PluggableWithAnywhereDoor) {
dynamic result;
if (pluginData.routeNameAndArgs != null) {
result = await pluginData.navigator?.pushNamed(
pluginData.routeNameAndArgs!.item1,
arguments: pluginData.routeNameAndArgs!.item2);
} else if (pluginData.route != null) {
result = await pluginData.navigator?.push(pluginData.route!);
}
pluginData.popResultReceive(result);
} else {
_currentSelected = pluginData;
if (_currentSelected != null) {
PluginManager.instance.activatePluggable(_currentSelected!);
}
_handleAction(_context, pluginData!);
if (widget.refreshChildLayout != null) {
widget.refreshChildLayout!();
}
pluginData.onTrigger();
}
};
_menuPage = MenuPage(
action: itemTapAction,
minimalAction: () {
_minimalContent = true;
_updatePanelWidget();
PluginStoreManager().storeMinimalToolbarSwitch(true);
},
closeAction: () {
_showedMenu = false;
_updatePanelWidget();
},
);
_toolbarWidget = ToolBarWidget(
action: itemTapAction,
maximalAction: () {
_minimalContent = false;
_updatePanelWidget();
PluginStoreManager().storeMinimalToolbarSwitch(false);
},
closeAction: () {
_showedMenu = false;
_updatePanelWidget();
},
);
_currentWidget = _empty;
}
@override
Widget build(BuildContext context) {
_context = context;
if (_windowSize.isEmpty) {
_dx = MediaQuery.of(context).size.width - dotSize.width - margin * 4;
_dy =
MediaQuery.of(context).size.height - dotSize.height - bottomDistance;
_windowSize = MediaQuery.of(context).size;
}
return Container(
width: _windowSize.width,
height: _windowSize.height,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
_currentWidget!,
Positioned(
left: _dx,
top: _dy,
child: Tooltip(
message: 'Open ume panel',
child: GestureDetector(
onTap: onTap,
onVerticalDragEnd: dragEnd,
onHorizontalDragEnd: dragEnd,
onHorizontalDragUpdate: dragEvent,
onVerticalDragUpdate: dragEvent,
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
const BoxShadow(
color: Colors.black12,
offset: Offset(0.0, 0.0),
blurRadius: 2.0,
spreadRadius: 1.0)
]),
width: dotSize.width,
height: dotSize.height,
child: Stack(
children: [
Center(
child: _logoWidget(),
),
Positioned(
right: 6,
top: 8,
child: RedDot(
pluginDatas: PluginManager
.instance.pluginsMap.values
.toList(),
))
],
),
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,289 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_ume/core/ui/icon_cache.dart';
import 'package:flutter_ume/core/pluggable_message_service.dart';
import 'package:flutter_ume/core/ui/panel_action_define.dart';
import 'package:flutter_ume/core/plugin_manager.dart';
import 'package:flutter_ume/core/red_dot.dart';
import 'package:flutter_ume/core/store_manager.dart';
import 'package:flutter_ume/core/pluggable.dart';
import 'package:flutter_ume/util/constants.dart';
class ToolBarWidget extends StatefulWidget {
ToolBarWidget({Key? key, this.action, this.maximalAction, this.closeAction})
: super(key: key);
final MenuAction? action;
final CloseAction? closeAction;
final MaximalAction? maximalAction;
@override
_ToolBarWidgetState createState() => _ToolBarWidgetState();
}
const double _dragBarHeight = 32;
const double _minimalHeight = 80;
class _ToolBarWidgetState extends State<ToolBarWidget> {
double _dy = 0;
late final double _maxDy;
@override
void initState() {
final bottomPadding = WidgetsBinding.instance.window.padding.bottom / ratio;
_maxDy =
windowSize.height - _minimalHeight - _dragBarHeight - bottomPadding;
_dy = _maxDy;
super.initState();
}
void _dragEvent(DragUpdateDetails details) {
_dy += details.delta.dy;
_dy = min(max(0, _dy), _maxDy);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Positioned(
left: 0,
top: _dy,
child: _ToolBarContent(
action: widget.action,
dragCallback: _dragEvent,
maximalAction: widget.maximalAction,
closeAction: widget.closeAction,
),
);
}
}
class _ToolBarContent extends StatefulWidget {
_ToolBarContent(
{Key? key,
this.action,
this.dragCallback,
this.maximalAction,
this.closeAction})
: super(key: key);
final MenuAction? action;
final Function? dragCallback;
final CloseAction? closeAction;
final MaximalAction? maximalAction;
@override
__ToolBarContentState createState() => __ToolBarContentState();
}
class __ToolBarContentState extends State<_ToolBarContent> {
PluginStoreManager _storeManager = PluginStoreManager();
List<Pluggable?> _dataList = [];
@override
void initState() {
super.initState();
_handleData();
}
@override
Widget build(BuildContext context) {
const cornerRadius = Radius.circular(10);
return Material(
borderRadius:
BorderRadius.only(topLeft: cornerRadius, topRight: cornerRadius),
elevation: 20,
child: Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.only(topLeft: cornerRadius, topRight: cornerRadius),
color: Color(0xffd0d0d0),
),
width: MediaQuery.of(context).size.width,
height: _minimalHeight + _dragBarHeight,
child: Column(
children: [
Container(
height: _dragBarHeight,
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Row(
children: [
InkWell(
onTap: () {
if (widget.closeAction != null) {
widget.closeAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: const Color(0xffff5a52),
)),
const SizedBox(
width: 8,
),
InkWell(
onTap: () {
if (widget.maximalAction != null) {
widget.maximalAction!();
}
},
child: const CircleAvatar(
radius: 10,
backgroundColor: const Color(0xff53c22b),
)),
Expanded(
child: GestureDetector(
onVerticalDragUpdate: (details) =>
_dragCallback(details),
child: Container(
height: _dragBarHeight,
color: const Color(0xffd0d0d0),
child: Center(
child: Text(
'UME',
style: const TextStyle(
color: Color(0xff575757),
fontSize: 14,
fontWeight: FontWeight.w600),
),
),
),
),
),
],
),
),
),
_PluginScrollContainer(
dataList: _dataList,
action: widget.action,
),
],
),
),
);
}
_dragCallback(DragUpdateDetails details) {
if (widget.dragCallback != null) widget.dragCallback!(details);
}
void _handleData() async {
List<Pluggable?> dataList = [];
List<String>? list = await _storeManager.fetchStorePlugins();
if (list == null || list.isEmpty) {
dataList = PluginManager.instance.pluginsMap.values.toList();
} else {
list.forEach((f) {
bool contain = PluginManager.instance.pluginsMap.containsKey(f);
if (contain) {
dataList.add(PluginManager.instance.pluginsMap[f]);
}
});
PluginManager.instance.pluginsMap.keys.forEach((key) {
if (!list.contains(key)) {
dataList.add(PluginManager.instance.pluginsMap[key]);
}
});
}
_saveData(dataList);
setState(() {
_dataList = dataList;
});
}
void _saveData(List<Pluggable?> data) {
List l = data.map((f) => f!.name).toList();
if (l.isEmpty) {
return;
}
Future.delayed(Duration(milliseconds: 500), () {
_storeManager.storePlugins(l as List<String>);
});
}
}
class _PluginScrollContainer extends StatelessWidget {
_PluginScrollContainer({Key? key, required this.dataList, this.action})
: super(key: key);
final List<Pluggable?> dataList;
final MenuAction? action;
@override
Widget build(BuildContext context) {
return Container(
height: _minimalHeight,
width: MediaQuery.of(context).size.width,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: dataList
.map(
(data) => _MenuCell(
pluginData: data,
action: action,
),
)
.toList(),
)));
}
}
class _MenuCell extends StatelessWidget {
const _MenuCell({Key? key, this.pluginData, this.action}) : super(key: key);
final Pluggable? pluginData;
final MenuAction? action;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
PluggableMessageService().resetCounter(pluginData!);
if (action != null) {
action!(pluginData);
}
},
child: Stack(
children: [
Container(
height: _minimalHeight,
width: _minimalHeight,
color: Colors.white,
child: Container(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: IconCache.icon(pluggableInfo: pluginData!),
height: 28,
width: 28),
Container(
margin: const EdgeInsets.only(top: 4),
child: Text(
pluginData!.name,
style:
const TextStyle(fontSize: 12, color: Colors.black),
maxLines: 1,
))
],
),
),
),
Positioned(
right: 8,
top: 8,
child: RedDot(
pluginDatas: [pluginData],
),
),
],
),
);
}
}